Fix agent errors

This commit is contained in:
Jon Staab 2026-07-27 16:20:27 -07:00
parent a2c5a8e2ff
commit 8953b0e04d
11 changed files with 52 additions and 37 deletions

View file

@ -124,11 +124,12 @@ Conventions below.
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
- Write a `{#if}`/`{:else if}` chain rather than hoisting display strings into a lookup `Record` in the script section.
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
- To read a tag, use `getTagValue(tagName, event.tags)` (or `getTagValues` for all matches) rather than reaching into the tag array yourself — that means no `getTag(tagName, event.tags)?.[1] || ""` and no `event.tags.find(nthEq(0, tagName))?.[1]`. `getTagValue` is exactly the latter, so the replacement is behavior-preserving, `undefined` included. Reserve `nthEq` for cases with no `getTag*` equivalent, such as `partition(nthEq(0, "imeta"), tags)`.
- Do not render a profile's `about` directly (e.g. `profile.about`); use the `ProfileAbout` component instead.
- Use `type Props` instead of interface when defining props for svelte components.
- When a component's value/prop shape mirrors a subset of an existing type, derive it with `Pick`/`Partial` and `export` that type from the component's `<script module>` (e.g. a `Values` type) for callers to import, instead of re-enumerating its sub-properties.
- Avoid pass-through functions except when the wrapper is part of an abstraction. `x => y()` is not ok, but `x => this.impl.y()` is ok, for example.
- For durations and timestamps, use `@welshman/lib`'s time helpers and constants (`MINUTE`, `HOUR`, `DAY`, `WEEK`, `MONTH`, `YEAR`) rather than raw milliseconds: `int(5, MINUTE)` for a duration in seconds, `ago(5, MINUTE)` for a past timestamp, `now()` for the current one, and `ms()`/`ms(int(...))` only where a browser API needs milliseconds. So `checkedAt < ago(5, MINUTE)` instead of `checkedAt < Date.now() - 300_000`. Write these count-first (`int(3, MONTH)`, `ago(2, WEEK)`) — the declared parameter order is `(unit, count)`, but the product is the same either way and every call site in this repo reads count-first. Nostr timestamps are seconds, so prefer `now()` over `Date.now()` for anything stored on an event.
- When declaring variables in a svelte component, the order should generally be: props, constants derived from props/state, functions declared with `const`, mutable variables declared with `let`, effects, onMount. This order may vary due to dependencies, but should generally be adhered to.
**Human-First Simplicity:**

View file

@ -19,7 +19,7 @@ import {addSpace, userSpaceUrls} from "@app/groups"
import {relaysMostlyRestricted} from "@app/policies"
import {broadcastUserData} from "@app/profiles"
import {Push} from "@app/push"
import {resyncApplicationData} from "@app/sync"
import {syncApplicationData} from "@app/sync"
import {notificationSettings, setSpaceNotifications} from "@app/settings"
import {deriveSocket} from "@app/relays"
@ -193,15 +193,11 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
return `Failed to connect`
}
// Relays send their challenge right after the socket opens, so if none shows up
// shortly the relay doesn't use auth and we can go ahead and publish.
await poll({
signal: AbortSignal.timeout(800),
signal: AbortSignal.timeout(3000),
condition: () => socket.auth.status === AuthStatus.Requested,
})
// Stop at any terminal status — retrying a denied signature or a forbidden response
// just re-prompts the user's signer for an answer we already have.
for (let i = 0; i < 3 && !authTerminalStatuses.includes(socket.auth.status); i++) {
await socket.auth.retryAuth(sign)
await waitForAuth(socket)
@ -298,7 +294,7 @@ export class Access {
await this.configureNotifications(notifications)
await addSpace(this.url)
this.clearRestricted()
resyncApplicationData()
syncApplicationData()
broadcastUserData([this.url])
}

View file

@ -1,6 +1,5 @@
<script lang="ts">
import {fromNostrURI} from "@welshman/util"
import {nthEq} from "@welshman/lib"
import {fromNostrURI, getTagValue} from "@welshman/util"
import {
parse,
truncate,
@ -107,7 +106,7 @@
const warning = $derived(
warningDismissed
? undefined
: $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
: $userSettingsValues.hide_sensitive && getTagValue("content-warning", event.tags),
)
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {

View file

@ -1,7 +1,6 @@
<script lang="ts">
import cx from "classnames"
import {fromNostrURI} from "@welshman/util"
import {nthEq} from "@welshman/lib"
import {fromNostrURI, getTagValue} from "@welshman/util"
import {
parse,
truncate,
@ -72,7 +71,7 @@
const warning = $derived(
warningDismissed
? undefined
: $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
: $userSettingsValues.hide_sensitive && getTagValue("content-warning", event.tags),
)
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {

View file

@ -20,7 +20,7 @@ import {
sortEventsAsc,
} from "@welshman/util"
import type {Filter, ManagementRequest, PublishedRoomMeta, TrustedEvent} from "@welshman/util"
import {first, simpleCache, sortBy, spec, uniq} from "@welshman/lib"
import {ago, first, MINUTE, now, simpleCache, sortBy, spec, uniq} from "@welshman/lib"
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
import {load} from "@welshman/net"
import {get} from "svelte/store"
@ -260,8 +260,8 @@ export const deriveUserIsSpaceAdmin = simpleCache(([url]: [string | undefined])
let checkedAt = 0
return readable(false, set => {
if (url && checkedAt < Date.now() - 300_000) {
checkedAt = Date.now()
if (url && checkedAt < ago(5, MINUTE)) {
checkedAt = now()
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []})
.then(({result}) => set(Boolean(result?.length)))

View file

@ -3,7 +3,6 @@ import {get} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {goto} from "$app/navigation"
import {page} from "$app/stores"
import {nthEq} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getAddress} from "@welshman/util"
import {tracker, userMessagingRelayList, getRelay} from "@welshman/app"
@ -206,9 +205,9 @@ export const getEventPath = (event: TrustedEvent, urls: string[]) => {
return makeMessagePath(url, event)
}
const address = event.tags.find(nthEq(0, "A"))?.[1]
const kind = event.tags.find(nthEq(0, "K"))?.[1]
const id = event.tags.find(nthEq(0, "E"))?.[1]
const address = getTagValue("A", event.tags)
const kind = getTagValue("K", event.tags)
const id = getTagValue("E", event.tags)
if (id && kind) {
if (parseInt(kind) === ZAP_GOAL) {

View file

@ -472,17 +472,14 @@ const syncDMs = () => {
let unsubscribe: Unsubscriber | undefined
export const syncApplicationData = () => {
unsubscribe?.()
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
unsubscribe = () => unsubscribers.forEach(call)
}
export const stopApplicationDataSync = () => {
unsubscribe?.()
unsubscribe = undefined
}
export const resyncApplicationData = () => {
stopApplicationDataSync()
syncApplicationData()
return () => {
unsubscribe?.()
unsubscribe = undefined
}
}

View file

@ -363,6 +363,29 @@ body.keyboard-open .room__compose {
@apply underline;
}
/* date picker. @svelte-plugins/datepicker renders its own markup, so these class
names appear nowhere in src/ — don't delete them on a grep. */
.picker {
--date-picker-foreground: var(--content);
--date-picker-background: var(--surface-more);
--date-picker-highlight-border: var(--primary);
--date-picker-selected-color: var(--primary-content);
--date-picker-selected-background: var(--primary);
}
.date-time-field {
@apply bg-surface rounded-2xl px-0;
border: var(--border-thin) solid var(--line);
}
.date-time-field input {
height: 100% !important;
width: 100% !important;
border-radius: var(--radius-2xl) !important;
border: none !important;
background: inherit !important;
color: inherit !important;
padding: 0.7rem 1rem !important;
}
/* tippy popover */
.tippy-target {
@apply z-tooltip pointer-events-none fixed inset-0;

View file

@ -39,4 +39,9 @@
@import "./space-menu.css";
@import "./tooltip.css";
/* SocketStatusIndicator builds its class as bg-{status.theme}, which the source
scanner can't see. These are the themes deriveSocketStatus can return. Kept
below every @import, since an @import after another at-rule is invalid. */
@source inline("bg-success bg-warning bg-error bg-gray-500");
@config "../../../tailwind.config.js";

View file

@ -38,7 +38,7 @@
import {getSetting, userSettings, notificationSettings} from "@app/settings"
import {DUFFLEPUD_URL, DEFAULT_RELAYS, INDEXER_RELAYS, POMADE_SIGNERS} from "@app/env"
import {pushState} from "@app/push/adapters/common"
import {syncApplicationData, resyncApplicationData, stopApplicationDataSync} from "@app/sync"
import {syncApplicationData} from "@app/sync"
import * as groups from "@app/groups"
import * as comments from "@app/comments"
import * as deletes from "@app/deletes"
@ -250,9 +250,7 @@
unsubscribers.push(() => defaultSocketPolicies.splice(-policies.length))
// History, navigation, application data
syncApplicationData()
unsubscribers.push(setupHistory(), setupAnalytics(), stopApplicationDataSync)
unsubscribers.push(setupHistory(), setupAnalytics(), syncApplicationData())
// Initialize keyboard state tracking
unsubscribers.push(syncKeyboard())
@ -277,7 +275,7 @@
if ($pubkey !== lastPubkey) {
lastPubkey = $pubkey
Pool.get().clear()
resyncApplicationData()
syncApplicationData()
}
}),
)

View file

@ -2,8 +2,6 @@
export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
darkMode: ["selector", '[data-theme="dark"]'],
// dynamic socket-status classes (bg-{$status.theme}) the scanner can't see
safelist: ["w-4", "h-4", "bg-success", "bg-warning", "bg-error", "bg-gray-500"],
theme: {
zIndex: {
none: 0,