@davait/aipictury 1.19.5 → 1.20.2
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/ext/about.js +2 -2
- package/ext/accountUi.js +83 -21
- package/ext/agent.js +84 -11
- package/ext/attachments.js +55 -0
- package/ext/cloud.js +165 -32
- package/ext/dialogs.js +9 -5
- package/ext/extension.js +281 -42
- package/ext/feedback.js +78 -6
- package/ext/firestore.js +37 -6
- package/ext/gpuRun.js +31 -7
- package/ext/keeping.js +171 -0
- package/ext/mediagen.js +186 -20
- package/ext/memory.js +17 -7
- package/ext/package.json +1 -1
- package/ext/plans.js +4 -1
- package/ext/remote.js +72 -12
- package/ext/voice.js +62 -11
- package/ext/workbench.js +9 -2
- package/main.js +96 -27
- package/package.json +1 -1
- package/vscode-shim.js +3 -0
package/ext/cloud.js
CHANGED
|
@@ -8,10 +8,21 @@ const crypto = require('crypto')
|
|
|
8
8
|
const zlib = require('zlib')
|
|
9
9
|
const path = require('path')
|
|
10
10
|
const { execFile } = require('child_process')
|
|
11
|
-
const { Firestore } = require('./firestore')
|
|
11
|
+
const { Firestore, fieldPath } = require('./firestore')
|
|
12
12
|
const { Storage } = require('./storage')
|
|
13
13
|
|
|
14
14
|
const CHUNK = 700_000 // characters of base64 a chunk (the rules allow 900,000)
|
|
15
|
+
// A conversation saved at every step is pushed at least this often, not only once it goes quiet:
|
|
16
|
+
// a run killed in its tenth minute had pushed nothing since it started (1.20.0).
|
|
17
|
+
const MAX_WAIT = 30_000
|
|
18
|
+
// A conversation shared by remote control is listed while its window hosts it: the window writes
|
|
19
|
+
// the listing again every REMOTE_BEAT, and a listing older than REMOTE_FRESH is no longer live —
|
|
20
|
+
// its window crashed or was killed, and nothing deleted it (1.20.0: such a listing was offered on
|
|
21
|
+
// aipictury.com for good).
|
|
22
|
+
const REMOTE_BEAT = 60_000
|
|
23
|
+
const REMOTE_FRESH = 150_000
|
|
24
|
+
/** Whether a remote-control listing is still kept up by its window. */
|
|
25
|
+
const remoteLive = (entry, now = Date.now()) => now - (Number(entry?.updated) || 0) <= REMOTE_FRESH
|
|
15
26
|
const MAX_CHUNKS = 64 // ~45 MB of base64: a conversation larger than that stays local
|
|
16
27
|
// Preferences that follow you. Nothing else is sent: never env, headers, tokens or MCP config.
|
|
17
28
|
const PREFERENCES = ['effort', 'approval', 'showThinking', 'focusView', 'autoMemory', 'notifications',
|
|
@@ -82,6 +93,16 @@ async function compactOn(server, assetId, onStage) {
|
|
|
82
93
|
return stats
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
// As frontend/src/rules.js: a list that would not fit is refused with the reason, never cut short
|
|
97
|
+
// without a word (slice dropped the newest rules, 1.20.0; firestore.rules refuses past 500).
|
|
98
|
+
function rulesFit(rules) {
|
|
99
|
+
if (rules.length > 500) throw new Error(`your account keeps 500 rules at most and this would make ${rules.length}`)
|
|
100
|
+
if (Buffer.byteLength(JSON.stringify(rules)) > 900_000) throw new Error('your rules are as long as can be kept together: remove or shorten one first')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const KEEP_POLL = 3000 // a render the server keeps: looked at this often
|
|
104
|
+
const KEEP_WATCH = 30 * 60_000 // and this long, before the outbox takes it up later
|
|
105
|
+
|
|
85
106
|
class Cloud {
|
|
86
107
|
/**
|
|
87
108
|
* `account`: account.js (who, and ID tokens). `device`: { id, name, app, platform, version }.
|
|
@@ -95,6 +116,10 @@ class Cloud {
|
|
|
95
116
|
this.storage = bucket ? new Storage({ bucket, emulator: storageEmulator, getToken: () => account.idToken(), fetchImpl }) : null
|
|
96
117
|
this.pushed = new Map() // conversation path → hash last pushed (in this process)
|
|
97
118
|
this.timers = new Map()
|
|
119
|
+
// Scheduled and not pushed yet, by conversation id: { folder, entry, owner, since } — kept
|
|
120
|
+
// here, so closing pushes them (flush) instead of dropping the timers' work (1.20.0).
|
|
121
|
+
this.pending = new Map()
|
|
122
|
+
this.scheduling = new Set() // schedule() calls still finding out who is signed in
|
|
98
123
|
// Signing out, or another account signing in, leaves nothing of the last session behind.
|
|
99
124
|
this.subscription = account.onChange?.(() => this.reset())
|
|
100
125
|
}
|
|
@@ -104,6 +129,7 @@ class Cloud {
|
|
|
104
129
|
reset() {
|
|
105
130
|
for (const t of this.timers.values()) clearTimeout(t)
|
|
106
131
|
this.timers.clear()
|
|
132
|
+
this.pending.clear()
|
|
107
133
|
this.pushed.clear()
|
|
108
134
|
}
|
|
109
135
|
|
|
@@ -150,6 +176,15 @@ class Cloud {
|
|
|
150
176
|
if (this.pushed.get(doc) === whole) return 'unchanged'
|
|
151
177
|
const before = await this.db.get(doc)
|
|
152
178
|
if (before?.hash === whole) { this.pushed.set(doc, whole); return 'unchanged' }
|
|
179
|
+
if (!before) {
|
|
180
|
+
// A new conversation: its project and a summary that says it is being saved go first, so
|
|
181
|
+
// its chunks always have a parent to be found by. A first save cut between the chunks and
|
|
182
|
+
// the summary left chunks under no conversation, which no delete ever listed (1.20.0).
|
|
183
|
+
// (No contentHash: a reader takes it for a conversation being saved, as it is.)
|
|
184
|
+
await this.touchProject(project, folder)
|
|
185
|
+
await this.db.set(doc, { ...summary, created: entry.created || Date.now(), updated: 0, chunks: 0,
|
|
186
|
+
origin: entry.origin || this.device.app, device: this.device.id })
|
|
187
|
+
}
|
|
153
188
|
const stale = []
|
|
154
189
|
if (before?.contentHash !== packed.hash) {
|
|
155
190
|
// The chunks first, the summary last: a reader that sees the summary finds every chunk.
|
|
@@ -173,26 +208,55 @@ class Cloud {
|
|
|
173
208
|
conversations: current?.conversations ?? 0 })
|
|
174
209
|
}
|
|
175
210
|
|
|
176
|
-
/** Pushes a few seconds after the last save, so a busy conversation is sent once, not at every
|
|
177
|
-
|
|
211
|
+
/** Pushes a few seconds after the last save, so a busy conversation is sent once, not at every
|
|
212
|
+
* step — and at least every MAX_WAIT while it keeps being saved. */
|
|
213
|
+
schedule(folder, entry, delayMs = 5000) {
|
|
214
|
+
// Known at once, so a flush right after a save (closing) waits for it rather than missing it.
|
|
215
|
+
const scheduling = this.scheduleAfterWho(folder, entry, delayMs)
|
|
216
|
+
this.scheduling.add(scheduling)
|
|
217
|
+
return scheduling.finally(() => this.scheduling.delete(scheduling))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async scheduleAfterWho(folder, entry, delayMs) {
|
|
178
221
|
// The push belongs to the account signed in now: if another one is by the time it runs, it is dropped.
|
|
179
222
|
const owner = await this.uid()
|
|
180
223
|
if (!owner) return
|
|
224
|
+
const was = this.pending.get(entry.id)
|
|
225
|
+
const since = was?.owner === owner ? was.since : Date.now()
|
|
226
|
+
this.pending.set(entry.id, { folder, entry, owner, since })
|
|
181
227
|
clearTimeout(this.timers.get(entry.id))
|
|
182
|
-
const timer = setTimeout(
|
|
228
|
+
const timer = setTimeout(() => {
|
|
183
229
|
this.timers.delete(entry.id)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}, delayMs)
|
|
230
|
+
this.pushPending(entry.id)
|
|
231
|
+
}, Math.max(0, Math.min(delayMs, since + MAX_WAIT - Date.now())))
|
|
187
232
|
timer.unref?.()
|
|
188
233
|
this.timers.set(entry.id, timer)
|
|
189
234
|
}
|
|
190
235
|
|
|
191
|
-
/**
|
|
192
|
-
async
|
|
236
|
+
/** The scheduled push of one conversation, now; never throws. */
|
|
237
|
+
async pushPending(id) {
|
|
238
|
+
const p = this.pending.get(id)
|
|
239
|
+
if (!p) return
|
|
240
|
+
this.pending.delete(id)
|
|
241
|
+
if ((await this.uid().catch(() => null)) !== p.owner) return
|
|
242
|
+
if ((p.entry.updated || 0) <= (this.clearedHere || 0)) return // saved before "Delete everything"
|
|
243
|
+
try { await this.pushConversation(p.folder, p.entry) } catch (error) { this.log(`cloud: ${id} not pushed: ${error.message}`) }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A conversation deleted here: a push still scheduled for it would bring it back. */
|
|
247
|
+
cancel(id) {
|
|
248
|
+
clearTimeout(this.timers.get(id))
|
|
249
|
+
this.timers.delete(id)
|
|
250
|
+
this.pending.delete(id)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** On closing: everything scheduled is pushed now, not dropped with its timer (1.20.0: VS Code
|
|
254
|
+
* never called this, and the last few seconds of every conversation stayed off the account). */
|
|
255
|
+
async flush() {
|
|
256
|
+
await Promise.allSettled([...this.scheduling])
|
|
193
257
|
for (const t of this.timers.values()) clearTimeout(t)
|
|
194
258
|
this.timers.clear()
|
|
195
|
-
for (const
|
|
259
|
+
for (const id of [...this.pending.keys()]) await this.pushPending(id)
|
|
196
260
|
}
|
|
197
261
|
|
|
198
262
|
/** A project's conversations, by its id (the web app's chats are the project "web"). */
|
|
@@ -206,6 +270,7 @@ class Cloud {
|
|
|
206
270
|
const doc = `${await this.base()}/projects/${projectId}/conversations/${id}`
|
|
207
271
|
const meta = await this.db.get(doc)
|
|
208
272
|
if (!meta) throw new Error('that conversation is not in your history any more')
|
|
273
|
+
if (!meta.contentHash) throw new Error('it is being saved right now — try again in a moment')
|
|
209
274
|
const parts = []
|
|
210
275
|
for (let i = 0; i < meta.chunks; i += 1) {
|
|
211
276
|
const c = await this.db.get(`${doc}/chunks/${i}`)
|
|
@@ -261,6 +326,7 @@ class Cloud {
|
|
|
261
326
|
const doc = `${project.path}/conversations/${id}`
|
|
262
327
|
const meta = await this.db.get(doc)
|
|
263
328
|
if (!meta) throw new Error('that conversation is not in the cloud')
|
|
329
|
+
if (!meta.contentHash) throw new Error('the cloud copy is being written — try again in a moment')
|
|
264
330
|
const parts = []
|
|
265
331
|
for (let i = 0; i < meta.chunks; i += 1) {
|
|
266
332
|
const c = await this.db.get(`${doc}/chunks/${i}`)
|
|
@@ -276,9 +342,10 @@ class Cloud {
|
|
|
276
342
|
async deleteConversation(folder, id) {
|
|
277
343
|
const project = await this.projectPath(folder)
|
|
278
344
|
const doc = `${project.path}/conversations/${id}`
|
|
279
|
-
const meta = await this.db.get(doc)
|
|
280
345
|
const writes = []
|
|
281
|
-
|
|
346
|
+
// Every chunk there is, listed, not as many as the summary says: a save cut short leaves
|
|
347
|
+
// chunks with no summary, or more than it counts (1.20.0).
|
|
348
|
+
for (const k of await this.db.list(`${doc}/chunks`)) writes.push({ delete: `${doc}/chunks/${k.id}` })
|
|
282
349
|
writes.push({ delete: doc })
|
|
283
350
|
await this.db.batch(writes)
|
|
284
351
|
this.pushed.delete(doc)
|
|
@@ -293,7 +360,21 @@ class Cloud {
|
|
|
293
360
|
|
|
294
361
|
/** Your rules: [{ id, text, created }]. */
|
|
295
362
|
async getRules() { return (await this.db.get(`${await this.base()}/memory/rules`))?.rules || [] }
|
|
296
|
-
async setRules(rules) { await this.db.set(`${await this.base()}/memory/rules`, { rules
|
|
363
|
+
async setRules(rules) { rulesFit(rules); await this.db.set(`${await this.base()}/memory/rules`, { rules, updated: Date.now() }) }
|
|
364
|
+
/** Rules added, and one taken out, on the server in one step each: a rule saved on another
|
|
365
|
+
* machine (or the web) at the same moment stays. Reading the list, changing it and writing it
|
|
366
|
+
* back lost one of the two (1.20.0). A rule is never edited, only added or removed, so taking
|
|
367
|
+
* out the very rules that carry this id removes it whatever was added meanwhile. */
|
|
368
|
+
async addRules(rules) {
|
|
369
|
+
if (!rules.length) return
|
|
370
|
+
rulesFit([...await this.getRules(), ...rules])
|
|
371
|
+
await this.db.change(`${await this.base()}/memory/rules`, { set: { updated: Date.now() }, append: { rules } })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async removeRule(id) {
|
|
375
|
+
const gone = (await this.getRules()).filter((r) => String(r.id) === String(id))
|
|
376
|
+
await this.db.change(`${await this.base()}/memory/rules`, { set: { updated: Date.now() }, remove: { rules: gone } })
|
|
377
|
+
}
|
|
297
378
|
|
|
298
379
|
async getPreferences() { return (await this.db.get(`${await this.base()}/settings/preferences`)) || null }
|
|
299
380
|
/** This device's preferences, merged into the account's: what other apps keep there (the web
|
|
@@ -305,27 +386,57 @@ class Cloud {
|
|
|
305
386
|
await this.db.set(path, { values: { ...current, ...allowed }, updated: Date.now(), device: this.device.id })
|
|
306
387
|
}
|
|
307
388
|
|
|
308
|
-
/** Adds one turn to the day's totals
|
|
389
|
+
/** Adds one turn to the day's totals — added on the server, so two machines' turns at the same
|
|
390
|
+
* moment both count (a read, a sum and a write back kept only one: 1.20.0). */
|
|
309
391
|
async addUsage(day, turn) {
|
|
310
392
|
const doc = `${await this.base()}/usage/${day}`
|
|
311
|
-
const
|
|
312
|
-
for (const t of turn.tools || [])
|
|
313
|
-
for (const m of turn.models || [])
|
|
314
|
-
await this.db.
|
|
315
|
-
genSeconds: Math.round((now.genSeconds + (turn.genSeconds || 0)) * 10) / 10, tools: now.tools, models: now.models, updated: Date.now() })
|
|
393
|
+
const increment = { turns: 1, prompt: turn.context || 0, output: turn.output || 0, genSeconds: Math.round((turn.genSeconds || 0) * 10) / 10 }
|
|
394
|
+
for (const t of turn.tools || []) increment[fieldPath('tools', t)] = (increment[fieldPath('tools', t)] || 0) + 1
|
|
395
|
+
for (const m of turn.models || []) increment[fieldPath('models', m)] = (increment[fieldPath('models', m)] || 0) + 1
|
|
396
|
+
await this.db.change(doc, { set: { updated: Date.now() }, increment })
|
|
316
397
|
}
|
|
317
398
|
|
|
318
|
-
/** Everything of yours in the cloud, deleted. The local files stay. */
|
|
319
399
|
/**
|
|
320
400
|
* A render the GPU server finished (its job), kept with the account as the web app keeps it:
|
|
321
401
|
* each file and a clip's poster in Storage (media/), its entry in renders/. Once per job;
|
|
322
|
-
* false when it was kept already or there is nothing to keep.
|
|
402
|
+
* false when it was kept already or there is nothing to keep (the job gone from the server).
|
|
403
|
+
* Safe to run again after a cut: the same files are written again, the entry still last.
|
|
323
404
|
*/
|
|
405
|
+
/** Whether this GPU server keeps renders with accounts by itself (GET /api/health `keeps`). */
|
|
406
|
+
async serverKeeps(server) {
|
|
407
|
+
const r = await fetch(`${server}/api/health`, { signal: AbortSignal.timeout(10_000) }).catch(() => null)
|
|
408
|
+
return r?.ok ? !!(await r.json()).keeps : false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** A render the server is keeping: how it goes, on its card. True once its entry is there; a
|
|
412
|
+
* failure throws, so the outbox looks again later. The app can go at any moment: the server goes on. */
|
|
413
|
+
async followKeeping(server, jobId, path, onStage) {
|
|
414
|
+
const until = Date.now() + KEEP_WATCH
|
|
415
|
+
while (Date.now() < until) {
|
|
416
|
+
const r = await fetch(`${server}/api/keeping/${jobId}`, { signal: AbortSignal.timeout(20_000) })
|
|
417
|
+
if (!r.ok) throw new Error(`the server could not say how keeping it goes (${r.status})`)
|
|
418
|
+
const now = await r.json()
|
|
419
|
+
if (now.state === 'unknown') return false
|
|
420
|
+
if (now.state === 'done') {
|
|
421
|
+
const entry = await this.db.get(path)
|
|
422
|
+
const stats = (entry?.assets || []).find((a) => a.stats)?.stats
|
|
423
|
+
onStage(now.asset, stats ? { stage: 'kept', stats } : { stage: 'kept' })
|
|
424
|
+
return !!entry
|
|
425
|
+
}
|
|
426
|
+
if (now.state === 'failed' || now.state === 'retry') throw new Error(now.error || 'the server could not keep it')
|
|
427
|
+
if (now.stage) onStage(now.asset, { stage: now.stage, progress: now.progress })
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, KEEP_POLL))
|
|
429
|
+
}
|
|
430
|
+
throw new Error('the server is still keeping it')
|
|
431
|
+
}
|
|
432
|
+
|
|
324
433
|
async keepRender(server, jobId, onStage = () => {}) {
|
|
325
434
|
if (!this.storage || !jobId) return false
|
|
326
435
|
const path = `${await this.base()}/renders/${jobId}`
|
|
327
436
|
if (await this.db.get(path)) return false
|
|
437
|
+
if (await this.serverKeeps(server)) return this.followKeeping(server, jobId, path, onStage)
|
|
328
438
|
const r = await fetch(`${server}/api/jobs/${jobId}`, { signal: AbortSignal.timeout(20_000) })
|
|
439
|
+
if (r.status === 404) return false
|
|
329
440
|
if (!r.ok) throw new Error(`the job could not be read (${r.status})`)
|
|
330
441
|
const job = await r.json()
|
|
331
442
|
if (job.status !== 'done') return false
|
|
@@ -365,13 +476,17 @@ class Cloud {
|
|
|
365
476
|
}
|
|
366
477
|
|
|
367
478
|
/** A conversation shared by remote control, listed with the account (aipictury.com offers to
|
|
368
|
-
* control it): its link, title, machine; gone when sharing stops.
|
|
369
|
-
|
|
370
|
-
|
|
479
|
+
* control it): its link, title, machine; gone when sharing stops. Written again every
|
|
480
|
+
* REMOTE_BEAT while it is hosted (`started` kept): `updated` says it is live. */
|
|
481
|
+
async putRemote(id, { url, title, device, conversation = '', started = Date.now() }) {
|
|
482
|
+
await this.db.set(`${await this.base()}/remote/${id}`, { url, title, device, conversation, started, updated: Date.now() })
|
|
371
483
|
}
|
|
372
484
|
|
|
373
485
|
async dropRemote(id) { await this.db.delete(`${await this.base()}/remote/${id}`) }
|
|
374
486
|
|
|
487
|
+
/** The shared conversations listed with the account: [{ id, url, title, device, conversation, … }]. */
|
|
488
|
+
async listRemote() { return (await this.db.list(`${await this.base()}/remote`)).map((d) => ({ ...d.data, id: d.id })) }
|
|
489
|
+
|
|
375
490
|
/** One kind of what is kept, deleted from every machine's cloud copy (its switch turned off):
|
|
376
491
|
* 'conversations' (with their chunks), 'memory' (your rules and each project's), 'settings'
|
|
377
492
|
* (preferences) or 'usage'. Returns how many records went. */
|
|
@@ -380,6 +495,8 @@ class Cloud {
|
|
|
380
495
|
const writes = []
|
|
381
496
|
let kept = null
|
|
382
497
|
if (kind === 'conversations' || kind === 'memory') {
|
|
498
|
+
// A save writes the project and a summary before any chunk (pushConversation), so what a
|
|
499
|
+
// cut-short save left is listed here, with its chunks.
|
|
383
500
|
for (const p of await this.db.list(`${base}/projects`)) {
|
|
384
501
|
if (kind === 'memory') { writes.push({ delete: `${base}/projects/${p.id}/memory/current` }); continue }
|
|
385
502
|
for (const c of await this.db.list(`${base}/projects/${p.id}/conversations`)) {
|
|
@@ -413,10 +530,29 @@ class Cloud {
|
|
|
413
530
|
await this.db.set(`${await this.base()}/settings/preferences`, { values: { gpuServer }, updated: Date.now(), device: this.device.id })
|
|
414
531
|
}
|
|
415
532
|
|
|
416
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Everything kept with the account, deleted (the local files stay). The "cleared" marker goes
|
|
535
|
+
* FIRST, with the server choice, in one commit: it used to be written last, so a delete cut
|
|
536
|
+
* between its commits or in the Storage step left no marker, and another machine's bringUp
|
|
537
|
+
* carried the deleted conversations straight back (1.20.0). Each step is safe to run again.
|
|
538
|
+
*/
|
|
539
|
+
async deleteEverything({ byServer = false } = {}) {
|
|
417
540
|
const base = await this.base()
|
|
418
541
|
const writes = []
|
|
419
542
|
const server = await this.serverChoice()
|
|
543
|
+
const cleared = Date.now()
|
|
544
|
+
// What was saved here in the last few seconds and is still scheduled is from before the delete:
|
|
545
|
+
// its push came after it and put the conversation back — the marker only stops bringUp (1.20.0).
|
|
546
|
+
// Cancelled now, and a push already on its way stops at pushPending.
|
|
547
|
+
this.reset()
|
|
548
|
+
this.clearedHere = cleared
|
|
549
|
+
// The preferences are replaced by the server choice alone, never deleted then put back.
|
|
550
|
+
await this.db.batch([{ set: [base, { cleared }] }, server
|
|
551
|
+
? { set: [`${base}/settings/preferences`, { values: { gpuServer: server }, updated: cleared, device: this.device.id }] }
|
|
552
|
+
: { delete: `${base}/settings/preferences` }])
|
|
553
|
+
// The server deletes the rest when it is asked, and carries on if this app goes (keeper.py): it
|
|
554
|
+
// also reaches what a signed-in app cannot list. The marker above already stops every device.
|
|
555
|
+
if (byServer) { this.pushed.clear(); return 0 }
|
|
420
556
|
for (const p of await this.db.list(`${base}/projects`)) {
|
|
421
557
|
for (const c of await this.db.list(`${base}/projects/${p.id}/conversations`)) {
|
|
422
558
|
// Every chunk there is, not as many as the summary says: one left by a save cut short stays otherwise.
|
|
@@ -428,16 +564,13 @@ class Cloud {
|
|
|
428
564
|
writes.push({ delete: `${base}/projects/${p.id}/memory/current` }, { delete: `${base}/projects/${p.id}` })
|
|
429
565
|
}
|
|
430
566
|
for (const col of ['usage', 'devices', 'renders', 'remote']) for (const d of await this.db.list(`${base}/${col}`)) writes.push({ delete: `${base}/${col}/${d.id}` })
|
|
431
|
-
writes.push({ delete: `${base}/memory/rules` }
|
|
567
|
+
writes.push({ delete: `${base}/memory/rules` })
|
|
568
|
+
this.pushed.clear()
|
|
432
569
|
await this.db.batch(writes)
|
|
433
570
|
// The media kept with the account (pictures, clips, attachments) go too.
|
|
434
571
|
if (this.storage) for (const o of await this.storage.list(`${base}/media/`)) { await this.storage.delete(o.name); writes.push(o) }
|
|
435
|
-
|
|
436
|
-
await this.db.set(base, { cleared: Date.now() })
|
|
437
|
-
if (server) await this.keepServerChoice(server)
|
|
438
|
-
this.pushed.clear()
|
|
439
|
-
return writes.length
|
|
572
|
+
return writes.length + 2
|
|
440
573
|
}
|
|
441
574
|
}
|
|
442
575
|
|
|
443
|
-
module.exports = { Cloud, projectOf, normalizeRemote, pack, unpack, PREFERENCES, CHUNK, MAX_CHUNKS }
|
|
576
|
+
module.exports = { Cloud, projectOf, normalizeRemote, pack, unpack, remoteLive, PREFERENCES, CHUNK, MAX_CHUNKS, MAX_WAIT, REMOTE_BEAT, REMOTE_FRESH }
|
package/ext/dialogs.js
CHANGED
|
@@ -750,10 +750,10 @@ async function sendReport(server, body) {
|
|
|
750
750
|
const response = await fetch(`${server}/api/feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
751
751
|
body: JSON.stringify(body), signal: controller.signal })
|
|
752
752
|
const data = await response.json().catch(() => ({}))
|
|
753
|
-
if (!response.ok) throw new Error(data.detail || `the server answered ${response.status}`)
|
|
753
|
+
if (!response.ok) throw Object.assign(new Error(data.detail || `the server answered ${response.status}`), { status: response.status })
|
|
754
754
|
return data.id
|
|
755
755
|
} catch (error) {
|
|
756
|
-
throw new Error(error.name === 'AbortError' ? 'the server did not answer in 10 s' : error.message, { cause: error })
|
|
756
|
+
throw Object.assign(new Error(error.name === 'AbortError' ? 'the server did not answer in 10 s' : error.message, { cause: error }), { status: error.status })
|
|
757
757
|
} finally { clearTimeout(timer) }
|
|
758
758
|
}
|
|
759
759
|
|
|
@@ -767,12 +767,16 @@ function reportHandler({ server, version, send = sendReport }) {
|
|
|
767
767
|
if (!description) throw new Error('say what happened')
|
|
768
768
|
const kind = f.kind === 'feedback' ? 'feedback' : 'bug'
|
|
769
769
|
saved = feedback.save({ kind, description, versions: { extension: version, vscode: vscode.version },
|
|
770
|
-
settings: settingsSnapshot(), log: log.tail() })
|
|
770
|
+
settings: settingsSnapshot(), log: log.tail(), send: f.send !== 'local' })
|
|
771
771
|
log.info(`problem report saved: ${saved.file}`)
|
|
772
772
|
let sent = 'Kept only on this machine.'
|
|
773
773
|
if (f.send !== 'local') {
|
|
774
|
-
try {
|
|
775
|
-
|
|
774
|
+
try {
|
|
775
|
+
sentId = await feedback.sendOne(server, saved.file, saved.body, send) // never twice with the outbox's retry
|
|
776
|
+
sent = `Sent to your AIPictury server as ${sentId}.`
|
|
777
|
+
} catch (error) {
|
|
778
|
+
// Not lost: it waits in the reports folder and goes by itself once the server answers.
|
|
779
|
+
sent = `Not sent yet: ${error.message}. It is saved here, and sent by itself once your server answers.`
|
|
776
780
|
log.warn('problem report not sent', error)
|
|
777
781
|
}
|
|
778
782
|
}
|