@threadcrumbs/threadnauts 1.1.0 → 1.3.0
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/NOTICES.md +96 -0
- package/README.md +4 -4
- package/bin/threadnauts.mjs +13 -7
- package/dist/assets/arrakesh-B6KYoRjN.svg +1 -0
- package/dist/assets/index-BzSHs41z.css +1 -0
- package/dist/assets/index-CASWItKa.js +2509 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-tight-latin-ext-wght-normal-B4u9PlvR.woff2 +0 -0
- package/dist/assets/inter-tight-latin-wght-normal-DX-nOvPD.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/shared-CudSvZVV.js +5015 -0
- package/dist/index.html +3 -3
- package/dist/pro-manifest.json +3 -3
- package/package.json +4 -3
- package/server/accents.mjs +27 -0
- package/server/api.mjs +55 -9
- package/server/http-server.mjs +8 -0
- package/server/threadgauge.mjs +98 -14
- package/dist/assets/arrakesh-BW9Bb56U.svg +0 -1
- package/dist/assets/index-BSPNlXcc.js +0 -2514
- package/dist/assets/index-DoVfuOrP.css +0 -1
- package/dist/assets/shared-CgqOYNxw.js +0 -4919
package/dist/index.html
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<meta name="color-scheme" content="dark" />
|
|
7
7
|
<title>Threadnauts</title>
|
|
8
8
|
<link rel="icon" href="/favicon.svg?app=threadnauts" type="image/svg+xml" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/shared-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CASWItKa.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/shared-CudSvZVV.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BzSHs41z.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="app"><div class="boot"><div class="inner">
|
package/dist/pro-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
3
|
{
|
|
4
|
-
"path": "assets/pro-
|
|
5
|
-
"sha256": "
|
|
6
|
-
"bytes":
|
|
4
|
+
"path": "assets/pro-DirsrB9B.js",
|
|
5
|
+
"sha256": "0040bc8fe354c96a4a81d290bde4a027502f9ea6a42ebe754dccce5c31f2c7df",
|
|
6
|
+
"bytes": 261373
|
|
7
7
|
}
|
|
8
8
|
]
|
|
9
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadcrumbs/threadnauts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A 3D colony sim where every coding-agent thread on your machine is a little bot building something.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
"icons": "node tools/build-world-icons.mjs",
|
|
50
50
|
"site": "node tools/build-site.mjs",
|
|
51
51
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
52
|
-
"prepack": "pnpm run build"
|
|
52
|
+
"prepack": "pnpm run build",
|
|
53
|
+
"auto-publish": "node ../common/tools/release.mjs"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@threadcrumbs/common": "^0.1.
|
|
56
|
+
"@threadcrumbs/common": "^0.1.7"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@gltf-transform/core": "^4.4.2",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const HEX_COLOR = /^#[0-9a-f]{6}$/
|
|
2
|
+
|
|
3
|
+
const asObject = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {})
|
|
4
|
+
|
|
5
|
+
function asHex(value) {
|
|
6
|
+
if (Number.isInteger(value) && value >= 0 && value <= 0xffffff) return `#${value.toString(16).padStart(6, '0')}`
|
|
7
|
+
if (typeof value === 'string' && HEX_COLOR.test(value.toLowerCase())) return value.toLowerCase()
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hexColors(...layers) {
|
|
12
|
+
const out = {}
|
|
13
|
+
for (const layer of layers) {
|
|
14
|
+
for (const [name, value] of Object.entries(asObject(layer))) {
|
|
15
|
+
const hex = asHex(value)
|
|
16
|
+
if (hex) Object.defineProperty(out, name, { value: hex, enumerable: true, writable: true, configurable: true })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function accentsAnswer(state) {
|
|
23
|
+
return {
|
|
24
|
+
orgs: hexColors(state?.accentsShown?.orgs, state?.accents?.orgs),
|
|
25
|
+
repos: hexColors(state?.accentsShown?.plots, state?.accents?.plots),
|
|
26
|
+
}
|
|
27
|
+
}
|
package/server/api.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import { openInTerminal } from './lib/terminal.mjs'
|
|
|
8
8
|
import { focusWindowOfPid } from './lib/windows.mjs'
|
|
9
9
|
import { localLicensing } from './license.mjs'
|
|
10
10
|
import { localProContent } from './pro-content.mjs'
|
|
11
|
-
import { findThreadgauge,
|
|
11
|
+
import { findThreadgauge, threadgaugeSessions } from './threadgauge.mjs'
|
|
12
|
+
import { accentsAnswer } from './accents.mjs'
|
|
12
13
|
import { organizationReader } from '@threadcrumbs/common/orgs'
|
|
13
14
|
import {
|
|
14
15
|
defaultHarness,
|
|
@@ -35,7 +36,7 @@ const proContent = () => (proFiles ??= localProContent())
|
|
|
35
36
|
export const proContentFile = (pathname) => proContent().file(pathname)
|
|
36
37
|
|
|
37
38
|
const withOrganizations = organizationReader()
|
|
38
|
-
const
|
|
39
|
+
const sessionsFromThreadgauge = threadgaugeSessions()
|
|
39
40
|
|
|
40
41
|
let about = null
|
|
41
42
|
const aboutThisApp = () =>
|
|
@@ -82,6 +83,7 @@ const emptyState = () => ({
|
|
|
82
83
|
viewedAt: {},
|
|
83
84
|
trinkets: {},
|
|
84
85
|
accents: {},
|
|
86
|
+
accentsShown: {},
|
|
85
87
|
districts: 0,
|
|
86
88
|
settings: null,
|
|
87
89
|
updatedAt: 0,
|
|
@@ -90,9 +92,26 @@ const emptyState = () => ({
|
|
|
90
92
|
const asObject = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {})
|
|
91
93
|
const asArray = (v) => (Array.isArray(v) ? v : [])
|
|
92
94
|
|
|
95
|
+
const BUSY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
|
|
96
|
+
const BUSY_WAITS_MS = [50, 150, 400]
|
|
97
|
+
const BUSY_MESSAGE = 'Another program has colony.json open. Threadnauts will try again.'
|
|
98
|
+
const isBusy = (err) => BUSY_CODES.has(err?.code)
|
|
99
|
+
|
|
100
|
+
async function whileBusy(operation) {
|
|
101
|
+
for (const wait of BUSY_WAITS_MS) {
|
|
102
|
+
try {
|
|
103
|
+
return await operation()
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (!isBusy(err)) throw err
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, wait))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return operation()
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
async function readState() {
|
|
94
113
|
try {
|
|
95
|
-
const raw = migrate(JSON.parse(await fsp.readFile(STATE_FILE, 'utf8')))
|
|
114
|
+
const raw = migrate(JSON.parse(await whileBusy(() => fsp.readFile(STATE_FILE, 'utf8'))))
|
|
96
115
|
return {
|
|
97
116
|
version: STATE_VERSION,
|
|
98
117
|
archived: asArray(raw.archived),
|
|
@@ -105,11 +124,13 @@ async function readState() {
|
|
|
105
124
|
viewedAt: asObject(raw.viewedAt),
|
|
106
125
|
trinkets: asObject(raw.trinkets),
|
|
107
126
|
accents: asObject(raw.accents),
|
|
127
|
+
accentsShown: asObject(raw.accentsShown),
|
|
108
128
|
districts: Number(raw.districts) || 0,
|
|
109
129
|
settings: raw.settings && typeof raw.settings === 'object' ? raw.settings : null,
|
|
110
130
|
updatedAt: Number(raw.updatedAt) || 0,
|
|
111
131
|
}
|
|
112
|
-
} catch {
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (isBusy(err)) throw err
|
|
113
134
|
return emptyState()
|
|
114
135
|
}
|
|
115
136
|
}
|
|
@@ -144,6 +165,7 @@ async function writeState(next) {
|
|
|
144
165
|
viewedAt: asObject(next.viewedAt),
|
|
145
166
|
trinkets: asObject(next.trinkets),
|
|
146
167
|
accents: asObject(next.accents),
|
|
168
|
+
accentsShown: asObject(next.accentsShown),
|
|
147
169
|
districts: Number(next.districts) || 0,
|
|
148
170
|
settings: next.settings && typeof next.settings === 'object' ? next.settings : null,
|
|
149
171
|
updatedAt: Date.now(),
|
|
@@ -154,7 +176,7 @@ async function writeState(next) {
|
|
|
154
176
|
// colony.json names the repos you work in and where they are: the owner's business, not
|
|
155
177
|
// every account on the machine's. A fresh file per write, so the mode always applies.
|
|
156
178
|
await fsp.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 0o600 })
|
|
157
|
-
await fsp.rename(tmp, STATE_FILE)
|
|
179
|
+
await whileBusy(() => fsp.rename(tmp, STATE_FILE))
|
|
158
180
|
} catch (err) {
|
|
159
181
|
await fsp.rm(tmp, { force: true }).catch(() => {})
|
|
160
182
|
throw err
|
|
@@ -403,10 +425,26 @@ function isLocalRequest(req) {
|
|
|
403
425
|
if (!LOCAL_HOSTS.has(hostnameOf(req.headers.host))) return false
|
|
404
426
|
|
|
405
427
|
const origin = req.headers.origin
|
|
406
|
-
if (origin && origin !== 'null') return
|
|
428
|
+
if (origin && origin !== 'null') return isOwnOrigin(origin, req)
|
|
407
429
|
return req.method === 'GET' || req.method === 'HEAD'
|
|
408
430
|
}
|
|
409
431
|
|
|
432
|
+
function isOwnOrigin(origin, req) {
|
|
433
|
+
try {
|
|
434
|
+
const url = new URL(origin)
|
|
435
|
+
const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80)
|
|
436
|
+
return LOCAL_HOSTS.has(hostnameOf(origin)) && port === req.socket.localPort
|
|
437
|
+
} catch {
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
|
|
443
|
+
const sendsJson = (req) => /^application\/json\s*(;|$)/i.test(req.headers['content-type'] || '')
|
|
444
|
+
|
|
445
|
+
const LOOPBACK_PEER = /^(127\.|::1$|::ffff:127\.)/
|
|
446
|
+
export const isAnotherDevice = (req) =>!LOOPBACK_PEER.test(req.socket.remoteAddress || '')
|
|
447
|
+
|
|
410
448
|
function readJsonBody(req, limit = 4 * 1024 * 1024) {
|
|
411
449
|
return new Promise((resolve, reject) => {
|
|
412
450
|
let size = 0
|
|
@@ -438,6 +476,9 @@ export async function middleware(req, res, next) {
|
|
|
438
476
|
if (!isLocalRequest(req)) {
|
|
439
477
|
return send(res, 403, { error: 'Threadnauts only answers its own page on this machine' })
|
|
440
478
|
}
|
|
479
|
+
if (WRITE_METHODS.has(req.method) && !sendsJson(req)) {
|
|
480
|
+
return send(res, 415, { error: 'Threadnauts only accepts JSON' })
|
|
481
|
+
}
|
|
441
482
|
|
|
442
483
|
try {
|
|
443
484
|
if (url.pathname === '/api/threads' && req.method === 'GET') {
|
|
@@ -477,7 +518,7 @@ export async function middleware(req, res, next) {
|
|
|
477
518
|
if (url.pathname === '/api/state' && req.method === 'PUT') {
|
|
478
519
|
const body = await readJsonBody(req)
|
|
479
520
|
const base = Number(body.baseUpdatedAt) || 0
|
|
480
|
-
return serialise(async () => {
|
|
521
|
+
return await serialise(async () => {
|
|
481
522
|
const current = await readState()
|
|
482
523
|
if (base && current.updatedAt !== base) return send(res, 409, current)
|
|
483
524
|
return send(res, 200, await writeState(body))
|
|
@@ -488,12 +529,16 @@ export async function middleware(req, res, next) {
|
|
|
488
529
|
return send(res, 200, await aboutThisApp())
|
|
489
530
|
}
|
|
490
531
|
|
|
532
|
+
if (url.pathname === '/api/accents' && req.method === 'GET') {
|
|
533
|
+
return send(res, 200, accentsAnswer(await readState()))
|
|
534
|
+
}
|
|
535
|
+
|
|
491
536
|
if (url.pathname === '/api/threadgauge' && req.method === 'GET') {
|
|
492
537
|
return send(res, 200, { url: await findThreadgauge() })
|
|
493
538
|
}
|
|
494
539
|
|
|
495
|
-
if (url.pathname === '/api/threadgauge/
|
|
496
|
-
return send(res, 200, await
|
|
540
|
+
if (url.pathname === '/api/threadgauge/sessions' && req.method === 'GET') {
|
|
541
|
+
return send(res, 200, await sessionsFromThreadgauge({ id: url.searchParams.get('id'), all: url.searchParams.has('all'), anotherDevice: isAnotherDevice(req) }))
|
|
497
542
|
}
|
|
498
543
|
|
|
499
544
|
if (url.pathname === '/api/pro' && req.method === 'GET') {
|
|
@@ -535,6 +580,7 @@ export async function middleware(req, res, next) {
|
|
|
535
580
|
|
|
536
581
|
return send(res, 404, { error: 'Unknown endpoint' })
|
|
537
582
|
} catch (err) {
|
|
583
|
+
if (isBusy(err)) return send(res, 503, { error: BUSY_MESSAGE })
|
|
538
584
|
return send(res, 500, { error: String(err && err.message ? err.message : err) })
|
|
539
585
|
}
|
|
540
586
|
}
|
package/server/http-server.mjs
CHANGED
|
@@ -110,6 +110,14 @@ async function serveRequest(req, res) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export function heldOnIpv6Loopback(port) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const trial = http.createServer()
|
|
116
|
+
trial.once('error', (err) => resolve(err.code === 'EADDRINUSE'))
|
|
117
|
+
trial.listen(port, '::1', () => trial.close(() => resolve(false)))
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
export function startServer({ port, host }) {
|
|
114
122
|
const server = http.createServer((req, res) => {
|
|
115
123
|
serveRequest(req, res).catch(() => {
|
package/server/threadgauge.mjs
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { DETAIL_LIMITS, clip, clipBlock } from '@threadcrumbs/common/lib/telemetry-fields'
|
|
2
|
+
|
|
1
3
|
const FIRST_PORT = 5274
|
|
2
4
|
const PORTS_TO_TRY = 20
|
|
3
5
|
const LOOPBACKS = ['127.0.0.1', '::1']
|
|
4
6
|
const PROBE_TIMEOUT_MS = 500
|
|
5
|
-
const
|
|
7
|
+
const SESSIONS_TIMEOUT_MS = 2000
|
|
8
|
+
const HISTORY_TIMEOUT_MS = 8000
|
|
9
|
+
const REUSE_OLDER_MS = 15000
|
|
6
10
|
const REUSE_FOUND_MS = 15000
|
|
11
|
+
const ASK_QUESTIONS = 4
|
|
12
|
+
const ASK_OPTIONS = 8
|
|
13
|
+
const LABEL_MAX = 160
|
|
14
|
+
const STATUS_MAX = 40
|
|
7
15
|
|
|
8
16
|
const originOf = (host, port) => `http://${host.includes(':') ? `[${host}]` : host}:${port}`
|
|
9
17
|
|
|
@@ -16,27 +24,103 @@ async function answersAsThreadgauge(origin, fetch) {
|
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
export async function
|
|
27
|
+
export async function locateThreadgauge({ fetch = globalThis.fetch, first = FIRST_PORT, tries = PORTS_TO_TRY, hosts = LOOPBACKS } = {}) {
|
|
20
28
|
const probes = []
|
|
21
29
|
for (let port = first; port < first + tries; port++) for (const host of hosts) probes.push({ port, origin: originOf(host, port) })
|
|
22
30
|
const found = await Promise.all(probes.map(({ origin }) => answersAsThreadgauge(origin, fetch)))
|
|
23
31
|
const index = found.indexOf(true)
|
|
24
|
-
return index === -1 ? null : `http://localhost:${probes[index].port}
|
|
32
|
+
return index === -1 ? null : { url: `http://localhost:${probes[index].port}/`, origin: probes[index].origin }
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
export const findThreadgauge = async (options) => (await locateThreadgauge(options))?.url ?? null
|
|
36
|
+
|
|
37
|
+
const text = (max) => (v) => (typeof v === 'string' ? clip(v, max) : null)
|
|
38
|
+
const block = (max) => (v) => (typeof v === 'string' ? clipBlock(v, max) : null)
|
|
39
|
+
const label = text(LABEL_MAX)
|
|
40
|
+
const words = block(DETAIL_LIMITS.errorText)
|
|
41
|
+
const number = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null)
|
|
42
|
+
const list = (v, max, keep) => (Array.isArray(v) ? v.filter((x) => x && typeof x === 'object').slice(0, max).map(keep) : null)
|
|
43
|
+
|
|
44
|
+
function keepAsk(ask) {
|
|
45
|
+
if (!ask || typeof ask !== 'object') return null
|
|
46
|
+
if (ask.type === 'question') {
|
|
47
|
+
const option = (o) => ({ label: label(o.label) ?? '', description: words(o.description) ?? '' })
|
|
48
|
+
const question = (q) => ({ header: label(q.header) ?? '', text: words(q.text) ?? '', options: list(q.options, ASK_OPTIONS, option) ?? [], multiple: q.multiple === true })
|
|
49
|
+
return { type: 'question', questions: list(ask.questions, ASK_QUESTIONS, question) ?? [] }
|
|
50
|
+
}
|
|
51
|
+
if (ask.type === 'approval') return { type: 'approval', tool: label(ask.tool) ?? '', target: label(ask.target) ?? '', kind: label(ask.kind) ?? 'other' }
|
|
52
|
+
if (ask.type === 'error') return { type: 'error', text: words(ask.text) ?? '' }
|
|
53
|
+
return { type: 'unknown' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SESSION_FIELDS = {
|
|
57
|
+
status: text(STATUS_MAX),
|
|
58
|
+
statusReason: text(STATUS_MAX),
|
|
59
|
+
ask: keepAsk,
|
|
60
|
+
context: (c) => (c && typeof c === 'object' ? { used: number(c.used), limit: number(c.limit) } : null),
|
|
61
|
+
tool: (t) => (t && typeof t === 'object' ? { name: label(t.name) ?? '', startedAt: number(t.startedAt), target: label(t.target) ?? '', kind: label(t.kind) ?? 'other' } : null),
|
|
62
|
+
turnStartedAt: number,
|
|
63
|
+
workingSubagents: (subs) => list(subs, DETAIL_LIMITS.subagents, (s) => ({ id: label(s.id) ?? '', name: label(s.name) ?? '' })),
|
|
64
|
+
awaiting: (threads) => list(threads, DETAIL_LIMITS.subagents, (t) => ({ id: label(t.id) ?? '', title: label(t.title) ?? '' })),
|
|
65
|
+
subagents: number,
|
|
66
|
+
live: (v) => v === true,
|
|
67
|
+
plan: (p) => (p && typeof p === 'object' ? { done: number(p.done), total: number(p.total), current: label(p.current) ?? '' } : null),
|
|
68
|
+
tokens: (t) => (t && typeof t === 'object' ? { output: number(t.output) } : null),
|
|
69
|
+
lines: (l) => (l && typeof l === 'object' ? { added: number(l.added), removed: number(l.removed) } : null),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function keepSessionFields(session) {
|
|
73
|
+
const kept = {}
|
|
74
|
+
for (const [field, keep] of Object.entries(SESSION_FIELDS)) if (field in session) kept[field] = keep(session[field])
|
|
75
|
+
return kept
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function withoutText(ask) {
|
|
79
|
+
if (!ask) return ask
|
|
80
|
+
if (ask.type === 'question') return { type: 'question', questions: [] }
|
|
81
|
+
if (ask.type === 'approval') return { type: 'approval', tool: ask.tool, target: '', kind: ask.kind }
|
|
82
|
+
if (ask.type === 'error') return { type: 'error', text: '' }
|
|
83
|
+
return ask
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function keepForAnotherDevice(kept) {
|
|
87
|
+
const out = { ...kept }
|
|
88
|
+
if ('ask' in out) out.ask = withoutText(out.ask)
|
|
89
|
+
if (out.tool) out.tool = { ...out.tool, target: '' }
|
|
90
|
+
if (out.awaiting) out.awaiting = out.awaiting.map(({ id }) => ({ id, title: '' }))
|
|
91
|
+
if (out.plan) out.plan = { ...out.plan, current: '' }
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const listOf = async (response) => {
|
|
96
|
+
const { sessions = [] } = response.ok ? await response.json() : {}
|
|
97
|
+
return (Array.isArray(sessions) ? sessions : []).filter((s) => s && typeof s.id === 'string')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function threadgaugeSessions({ fetch = globalThis.fetch, find = () => locateThreadgauge({ fetch }), now = Date.now } = {}) {
|
|
101
|
+
let found = { at: -Infinity, where: null }
|
|
102
|
+
let older = { at: -Infinity, origin: null, list: [] }
|
|
103
|
+
async function olderList(origin) {
|
|
104
|
+
if (older.origin === origin && now() - older.at <= REUSE_OLDER_MS) return older.list
|
|
105
|
+
const list = await listOf(await fetch(`${origin}/api/history`, { signal: AbortSignal.timeout(HISTORY_TIMEOUT_MS) }))
|
|
106
|
+
older = { at: now(), origin, list }
|
|
107
|
+
return list
|
|
108
|
+
}
|
|
109
|
+
return async function sessions({ id = null, all = false, anotherDevice = false } = {}) {
|
|
110
|
+
if (now() - found.at > REUSE_FOUND_MS) found = { at: now(), where: await find() }
|
|
111
|
+
const where = found.where
|
|
112
|
+
if (!where) return { url: null, sessions: {} }
|
|
113
|
+
if (!id && !all) return { url: where.url, sessions: {} }
|
|
32
114
|
try {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
115
|
+
const live = await listOf(await fetch(`${where.origin}/api/sessions`, { signal: AbortSignal.timeout(SESSIONS_TIMEOUT_MS) }))
|
|
116
|
+
const listed = new Set(live.map((s) => s.id))
|
|
117
|
+
const everything = all ? [...live, ...(await olderList(where.origin)).filter((s) => !listed.has(s.id))] : live
|
|
118
|
+
const wanted = everything.filter((s) => all || s.id === id)
|
|
119
|
+
const keep = (s) => (anotherDevice ? keepForAnotherDevice(keepSessionFields(s)) : keepSessionFields(s))
|
|
120
|
+
return { url: where.url, sessions: Object.fromEntries(wanted.map((s) => [s.id, keep(s)])) }
|
|
37
121
|
} catch {
|
|
38
|
-
found = {
|
|
39
|
-
return { url: null,
|
|
122
|
+
found = { at: -Infinity, where: null }
|
|
123
|
+
return { url: null, sessions: {} }
|
|
40
124
|
}
|
|
41
125
|
}
|
|
42
126
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" class="world-icon" viewBox="0 0 100 100" aria-hidden="true"><defs><clipPath id="wi-frameg"><circle cx="50" cy="50" r="50"/></clipPath><linearGradient id="wi-l0" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86a6c6"/><stop offset="0.58" stop-color="#c7c9c5"/><stop offset="1" stop-color="#f2e0c4"/></linearGradient><radialGradient id="wi-r1" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"><stop offset="0" stop-color="#f6e9d6" stop-opacity="0.3"/><stop offset="0.4" stop-color="#f6e9d6" stop-opacity="0.13"/><stop offset="1" stop-color="#f6e9d6" stop-opacity="0"/></radialGradient><radialGradient id="wi-r2" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"><stop offset="0" stop-color="#fff6e4" stop-opacity="0.6"/><stop offset="0.4" stop-color="#fff6e4" stop-opacity="0.25"/><stop offset="1" stop-color="#fff6e4" stop-opacity="0"/></radialGradient><clipPath id="wi-c3"><circle cx="31.1" cy="33.2" r="6.03"/></clipPath><radialGradient id="wi-r4" cx="0.66" cy="0.4" r="0.72" fx="0.66" fy="0.4"><stop offset="0" stop-color="#e3ddd3"/><stop offset="0.7" stop-color="#dcd4c8"/><stop offset="1" stop-color="#aca59c"/></radialGradient><linearGradient id="wi-l5" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#000" stop-opacity="0.32"/><stop offset="0.5" stop-color="#000" stop-opacity="0.1"/><stop offset="0.78" stop-color="#000" stop-opacity="0"/></linearGradient><clipPath id="wi-c6"><circle cx="43.7" cy="28" r="4.92"/></clipPath><radialGradient id="wi-r7" cx="0.66" cy="0.4" r="0.72" fx="0.66" fy="0.4"><stop offset="0" stop-color="#d3c6b6"/><stop offset="0.7" stop-color="#c8b8a4"/><stop offset="1" stop-color="#9c9080"/></radialGradient><linearGradient id="wi-l8" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#000" stop-opacity="0.32"/><stop offset="0.5" stop-color="#000" stop-opacity="0.1"/><stop offset="0.78" stop-color="#000" stop-opacity="0"/></linearGradient><radialGradient id="wi-r9" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"><stop offset="0" stop-color="#ffffff"/><stop offset="0.55" stop-color="#fffbf4"/><stop offset="1" stop-color="#fff6e4"/></radialGradient><linearGradient id="wi-la" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f2dcb5"/><stop offset="0.35" stop-color="#ebcf9e"/><stop offset="1" stop-color="#c7a06e"/></linearGradient><linearGradient id="wi-lb" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f3deb7"/><stop offset="0.35" stop-color="#eccf9c"/><stop offset="1" stop-color="#c29965"/></linearGradient><linearGradient id="wi-lc" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f5e1ba"/><stop offset="0.35" stop-color="#ecce9a"/><stop offset="1" stop-color="#ba8f59"/></linearGradient><linearGradient id="wi-ld" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6e3bc"/><stop offset="0.35" stop-color="#ecce98"/><stop offset="1" stop-color="#b4864e"/></linearGradient><radialGradient id="wi-re" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"><stop offset="0" stop-color="#ff6a2a" stop-opacity="0.9"/><stop offset="0.4" stop-color="#ff6a2a" stop-opacity="0.38"/><stop offset="1" stop-color="#ff6a2a" stop-opacity="0"/></radialGradient><linearGradient id="wi-lf" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ead0a8" stop-opacity="0"/><stop offset="0.55" stop-color="#ead0a8" stop-opacity="0.35"/><stop offset="1" stop-color="#ead0a8" stop-opacity="0"/></linearGradient><radialGradient id="wi-rh" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"><stop offset="0.62" stop-color="#000" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity="0.3"/></radialGradient></defs><g clip-path="url(#wi-frameg)"><rect width="100" height="100" fill="url(#wi-l0)"/><ellipse cx="75" cy="62" rx="70" ry="14" fill="url(#wi-r1)"/><circle cx="75" cy="24" r="16.2" fill="url(#wi-r2)"/><circle cx="75" cy="24" r="4.5" fill="url(#wi-r9)"/><g opacity="0.6"><circle cx="31.1" cy="33.2" r="6.03" fill="url(#wi-r4)"/><g clip-path="url(#wi-c3)"><ellipse cx="30.94" cy="36.85" rx="0.49" ry="0.35" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(12.36 31.1 33.2)"/><ellipse cx="31.22" cy="29.7" rx="0.49" ry="0.24" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(14.59 31.1 33.2)"/><ellipse cx="28.65" cy="34.93" rx="0.94" ry="0.52" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(-5.2 31.1 33.2)"/><ellipse cx="30.21" cy="34.27" rx="0.95" ry="0.55" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(-17.63 31.1 33.2)"/><ellipse cx="27.06" cy="32.41" rx="1.39" ry="1.07" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(-0.55 31.1 33.2)"/><ellipse cx="30.82" cy="32.39" rx="1.07" ry="0.56" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(-29.66 31.1 33.2)"/><ellipse cx="34.3" cy="35.57" rx="1.2" ry="0.6" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(-11.43 31.1 33.2)"/><ellipse cx="28.24" cy="34.89" rx="1.03" ry="0.66" fill="#9a8e7e" fill-opacity="0.42" transform="rotate(10.85 31.1 33.2)"/><ellipse cx="29.59" cy="33.8" rx="1.93" ry="1.33" fill="#4e463e" fill-opacity=".7"/><circle cx="29.07" cy="31.74" r="0.49" fill="#aca59c"/><path d="M28.59 31.74A0.49 0.49 0 0 0 29.56 31.74" fill="none" stroke="#e7e1d9" stroke-width="0.17"/><circle cx="34.33" cy="33.54" r="0.89" fill="#aca59c"/><path d="M33.44 33.54A0.89 0.89 0 0 0 35.22 33.54" fill="none" stroke="#e7e1d9" stroke-width="0.31"/><circle cx="29.71" cy="34.16" r="0.88" fill="#aca59c"/><path d="M28.83 34.16A0.88 0.88 0 0 0 30.59 34.16" fill="none" stroke="#e7e1d9" stroke-width="0.31"/><rect x="25.07" y="27.17" width="12.07" height="12.07" fill="url(#wi-l5)"/></g><circle cx="31.1" cy="33.2" r="5.73" fill="none" stroke="#fff4e4" stroke-opacity=".35" stroke-width=".6"/></g><g opacity="0.6"><circle cx="43.7" cy="28" r="4.92" fill="url(#wi-r7)"/><g clip-path="url(#wi-c6)"><ellipse cx="43.4" cy="27.14" rx="0.98" ry="0.44" fill="#988874" fill-opacity="0.36" transform="rotate(-26.08 43.7 28)"/><ellipse cx="45.69" cy="26.96" rx="0.92" ry="0.73" fill="#988874" fill-opacity="0.36" transform="rotate(14.56 43.7 28)"/><ellipse cx="42.25" cy="26.6" rx="1.13" ry="0.64" fill="#988874" fill-opacity="0.36" transform="rotate(26.21 43.7 28)"/><ellipse cx="46.27" cy="30.8" rx="1.09" ry="0.51" fill="#988874" fill-opacity="0.36" transform="rotate(-7.29 43.7 28)"/><ellipse cx="42.87" cy="28.47" rx="0.69" ry="0.32" fill="#988874" fill-opacity="0.36" transform="rotate(-11.31 43.7 28)"/><ellipse cx="44.23" cy="25.53" rx="1.17" ry="0.83" fill="#988874" fill-opacity="0.36" transform="rotate(27.81 43.7 28)"/><ellipse cx="43.69" cy="25.53" rx="0.54" ry="0.41" fill="#988874" fill-opacity="0.36" transform="rotate(-2.78 43.7 28)"/><ellipse cx="46.72" cy="26.08" rx="1.14" ry="0.51" fill="#988874" fill-opacity="0.36" transform="rotate(-1.01 43.7 28)"/><circle cx="44.08" cy="26.22" r="0.54" fill="#9c9080"/><path d="M43.54 26.22A0.54 0.54 0 0 0 44.63 26.22" fill="none" stroke="#d9cdbf" stroke-width="0.19"/><circle cx="44.48" cy="30.7" r="0.75" fill="#9c9080"/><path d="M43.73 30.7A0.75 0.75 0 0 0 45.23 30.7" fill="none" stroke="#d9cdbf" stroke-width="0.26"/><circle cx="46.47" cy="29.29" r="0.71" fill="#9c9080"/><path d="M45.76 29.29A0.71 0.71 0 0 0 47.18 29.29" fill="none" stroke="#d9cdbf" stroke-width="0.25"/><rect x="38.78" y="23.08" width="9.85" height="9.85" fill="url(#wi-l8)"/></g><circle cx="43.7" cy="28" r="4.62" fill="none" stroke="#fff0dc" stroke-opacity=".35" stroke-width=".6"/></g><path d="M-3 64L-1.96 64L0.54 60.58L1.14 57.27L21.1 57.27L21.7 60.58L24.2 64L31.17 64L33.67 60.01L34.27 55.38L62.07 55.38L62.67 60.01L65.17 64L71.65 64L74.15 60.33L74.75 56.42L97.19 56.42L97.79 60.33L100.29 64L103 64Z" fill="#b9926b"/><path d="M0.84 58.84H21.4M0.84 60.42H21.4M33.97 57.59H62.37M33.97 59.79H62.37M74.45 58.28H97.49M74.45 60.14H97.49" fill="none" stroke="#987858" stroke-width=".5" stroke-opacity=".7"/><rect x="0" y="56" width="100" height="10" fill="url(#wi-lf)"/><path d="M-2 62H102V70H-2Z" fill="#cca777"/><path d="M-2 64.2C4 60 20 60.3 32 63.6S64 59.4 102 63.3V102H-2Z" fill="url(#wi-la)"/><path d="M-2 64.2C4 60 20 60.3 32 63.6S64 59.4 102 63.3" fill="none" stroke="#f9ebcd" stroke-opacity=".55" stroke-width=".7"/><path d="M-2 70.8C24 64.5 40 64.95 52 69.9S84 63.6 102 69.45V102H-2Z" fill="url(#wi-lb)"/><path d="M-2 70.8C24 64.5 40 64.95 52 69.9S84 63.6 102 69.45" fill="none" stroke="#f9ebcd" stroke-opacity=".55" stroke-width=".7"/><path d="M-2 79.4C4 71 20 71.6 32 78.2S64 69.8 102 77.6V102H-2Z" fill="url(#wi-lc)"/><path d="M-2 79.4C4 71 20 71.6 32 78.2S64 69.8 102 77.6" fill="none" stroke="#f9ebcd" stroke-opacity=".55" stroke-width=".7"/><path d="M-2 91.2C24 80 40 80.8 52 89.6S84 78.4 102 88.8V102H-2Z" fill="url(#wi-ld)"/><path d="M-2 91.2C24 80 40 80.8 52 89.6S84 78.4 102 88.8" fill="none" stroke="#f9ebcd" stroke-opacity=".55" stroke-width=".7"/><ellipse cx="33.35" cy="89.98" rx="5.53" ry="0.88" fill="#8e6a46" fill-opacity="0.61"/><ellipse cx="10.83" cy="72.12" rx="3.84" ry="0.61" fill="#8e6a46" fill-opacity="0.61"/><ellipse cx="50.09" cy="89.96" rx="6.76" ry="1.55" fill="#8e6a46" fill-opacity="0.61"/><ellipse cx="80.31" cy="87.95" rx="6.35" ry="1.5" fill="#8e6a46" fill-opacity="0.61"/><ellipse cx="83.2" cy="87.75" rx="5.87" ry="0.73" fill="#c4643a" fill-opacity="0.5"/><ellipse cx="11.21" cy="96.11" rx="6.95" ry="1.55" fill="#c4643a" fill-opacity="0.5"/><ellipse cx="12.87" cy="94.71" rx="5.66" ry="1.56" fill="#c4643a" fill-opacity="0.5"/><ellipse cx="87.26" cy="91.64" rx="4.63" ry="1.65" fill="#c4643a" fill-opacity="0.5"/><path d="M82.7 72L83.3 69L83.55 66.5L83.9 65L84.3 67L84.8 68.75L85.3 72Z" fill="#6c4c31"/><path d="M83.9 65L84.3 67L84.8 68.75L85.3 72L84.3 72Z" fill="#9a6c46"/><path d="M12.57 74L13.23 70.7L13.51 67.95L13.89 66.3L14.33 68.5L14.88 70.43L15.43 74Z" fill="#6c4c31"/><path d="M13.89 66.3L14.33 68.5L14.88 70.43L15.43 74L14.33 74Z" fill="#9a6c46"/><path d="M23.79 92L24.81 86.9L25.24 82.65L25.83 80.1L26.51 83.5L27.36 86.48L28.21 92Z" fill="#6c4c31"/><path d="M25.83 80.1L26.51 83.5L27.36 86.48L28.21 92L26.51 92Z" fill="#9a6c46"/><path d="M77.12 93L77.66 90.84L79.28 89.76L81.44 90.12L82.88 91.74L83.06 93Z" fill="#9a6c46"/><path d="M77.66 90.84L79.28 89.76L80.54 91L78.56 91.92Z" fill="#b8987e"/><circle cx="50.72" cy="73.64" r="1.6" fill="url(#wi-re)"/><circle cx="15.9" cy="75.19" r="1.6" fill="url(#wi-re)"/><circle cx="52.57" cy="83.81" r="1.6" fill="url(#wi-re)"/><circle cx="19.26" cy="79.5" r="1.6" fill="url(#wi-re)"/><circle cx="85.88" cy="74.14" r="1.6" fill="url(#wi-re)"/><circle cx="59.31" cy="90.22" r="1.6" fill="url(#wi-re)"/><path d="M68.72 24.47l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M17.44 42.28l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M79.77 80.74l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M7.88 95.69l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M11.31 43.26l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M50.55 14.52l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M45.9 84.46l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M50.7 84.42l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M52.72 9.31l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M37.63 45.33l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M15.45 82.88l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><path d="M86.3 33.96l-2 0.3" stroke="#f0d8a8" stroke-opacity=".7" stroke-width=".35" stroke-linecap="round"/><circle cx="91.1" cy="95.63" r="0.25" fill="#d8b090" fill-opacity=".85"/><circle cx="86.84" cy="59.61" r="0.34" fill="#d8b090" fill-opacity=".85"/><circle cx="63.36" cy="22.07" r="0.3" fill="#d8b090" fill-opacity=".85"/><rect width="100" height="100" fill="url(#wi-rh)"/></g><circle cx="50" cy="50" r="49.2" fill="none" stroke="#fff" stroke-opacity=".22" stroke-width="1.6"/></svg>
|