fix(mesh,ui,fedimint): mesh-AI chat trigger + transport-aware reply, stop ARCHY:2 public-channel spam, AI allowlist + model dropdown, Fedimint client manifest, settings reorder, chat scroll

- mesh: stop broadcasting ARCHY:2 identity on the public channel (startup + every advert tick); receive path still parses inbound. No more public-channel spam.
- mesh assistant: trigger on !ai/!ask typed in 1:1 chat (was only the dead AssistQuery path + bare channel text); route the reply transport-aware via MeshService::send_message (Tor for federation peers, LoRa for radio) through a new AssistChatReply event consumed at the server layer — fixes replies never reaching federation askers.
- mesh assistant: per-contact !ai allowlist (allowed_contacts) bypassing trusted_only; config + RPC + is_sender_allowed.
- fedimint-clientd manifest: network_policy open -> bridge (invalid value made the loader skip the whole manifest, so fmcd never ran and federations never joined/listed).
- ui: AI panel — Claude model dropdown (Haiku/Sonnet/Opus presets) + allowlist contact picker.
- ui: Settings — App Updates + App Registry moved under Account.
- ui: mesh chat — overscroll-behavior: contain so chat scroll no longer bleeds to the contacts panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-18 03:33:37 -04:00
co-authored by Claude Opus 4.8
parent 2a017623e9
commit 3a21243be7
15 changed files with 242 additions and 32 deletions
+2
View File
@@ -130,6 +130,7 @@ export interface AssistantStatus {
model: string | null
trusted_only: boolean
backend: string
allowed_contacts: string[]
default_model: string
ollama_detected: boolean
claude_available: boolean
@@ -613,6 +614,7 @@ export const useMeshStore = defineStore('mesh', () => {
model?: string | null
trusted_only?: boolean
backend?: string
allowed_contacts?: string[]
}) {
const res = await rpcClient.call<Partial<AssistantStatus>>({
method: 'mesh.assistant-configure',
+4
View File
@@ -1,11 +1,15 @@
<script setup lang="ts">
import AccountSection from '@/views/settings/AccountSection.vue'
import SystemUpdatesSection from '@/views/settings/SystemUpdatesSection.vue'
import AppRegistriesSection from '@/views/settings/AppRegistriesSection.vue'
import SystemSection from '@/views/settings/SystemSection.vue'
</script>
<template>
<div class="pb-6">
<AccountSection />
<SystemUpdatesSection />
<AppRegistriesSection />
<SystemSection />
</div>
</template>
+63 -1
View File
@@ -11,6 +11,23 @@ const enabled = ref(false)
const model = ref('') // '' = use the backend's default model
const policy = ref<'trusted' | 'anyone'>('trusted')
const backend = ref<'claude' | 'ollama'>('claude')
const allowedContacts = ref<string[]>([])
// Preset Claude models offered in the dropdown ('' = backend default = Haiku).
const CLAUDE_MODELS: { value: string; label: string }[] = [
{ value: '', label: 'Default (Claude Haiku 4.5)' },
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5 — fast & cheap' },
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 — balanced' },
{ value: 'claude-opus-4-8', label: 'Claude Opus 4.8 — most capable' },
]
// Include any non-preset value the node already has so it isn't silently lost.
const claudeModelOptions = computed(() => {
const opts = [...CLAUDE_MODELS]
if (model.value && !opts.some((o) => o.value === model.value)) {
opts.push({ value: model.value, label: `${model.value} (custom)` })
}
return opts
})
// Sync local controls from the fetched status.
watch(
@@ -21,10 +38,30 @@ watch(
model.value = s.model ?? ''
policy.value = s.trusted_only ? 'trusted' : 'anyone'
backend.value = s.backend === 'ollama' ? 'ollama' : 'claude'
allowedContacts.value = [...(s.allowed_contacts ?? [])]
},
{ immediate: true },
)
// Addressable contacts (have an archipelago/radio pubkey) for the allowlist.
const contactOptions = computed(() =>
mesh.peers
.filter((p) => !!p.pubkey_hex)
.map((p) => ({ pubkey: p.pubkey_hex as string, name: p.advert_name || (p.pubkey_hex as string).slice(0, 10) })),
)
function isAllowed(pubkey: string) {
return allowedContacts.value.some((k) => k.toLowerCase() === pubkey.toLowerCase())
}
function toggleAllowed(pubkey: string) {
if (isAllowed(pubkey)) {
allowedContacts.value = allowedContacts.value.filter((k) => k.toLowerCase() !== pubkey.toLowerCase())
} else {
allowedContacts.value = [...allowedContacts.value, pubkey]
}
apply({ allowed_contacts: allowedContacts.value })
}
onMounted(() => {
mesh.fetchAssistantStatus()
})
@@ -45,6 +82,7 @@ async function apply(partial: {
model?: string | null
trusted_only?: boolean
backend?: string
allowed_contacts?: string[]
}) {
saving.value = true
try {
@@ -131,7 +169,9 @@ function onPolicy() {
</div>
<div v-else class="mesh-assistant-field">
<label class="mesh-bitcoin-label">Model</label>
<input v-model="model" class="mesh-bitcoin-input mesh-bitcoin-input-sm" :placeholder="defaultModel" @change="onModel" />
<select v-model="model" class="mesh-bitcoin-input mesh-bitcoin-input-sm" @change="onModel">
<option v-for="m in claudeModelOptions" :key="m.value" :value="m.value">{{ m.label }}</option>
</select>
</div>
<div class="mesh-assistant-field">
@@ -147,6 +187,28 @@ function onPolicy() {
</p>
</div>
<!-- Per-contact allowlist: let specific contacts use !ai even when the
policy is "trusted only" and they aren't federation-trusted. -->
<div class="mesh-assistant-field">
<label class="mesh-bitcoin-label">Always allow these contacts</label>
<p class="text-xs text-white/40 mb-2">
Listed contacts can use <code>!ai</code> regardless of the policy above.
</p>
<div v-if="contactOptions.length === 0" class="text-xs text-white/40">
No contacts yet they appear here once you have mesh/federation contacts.
</div>
<div v-else class="mesh-assistant-allowlist">
<label
v-for="c in contactOptions"
:key="c.pubkey"
class="mesh-assistant-allow-row"
>
<input type="checkbox" :checked="isAllowed(c.pubkey)" @change="toggleAllowed(c.pubkey)" />
<span class="mesh-assistant-allow-name">{{ c.name }}</span>
</label>
</div>
</div>
<p class="text-xs text-white/50 mt-2">
Ask from any client by sending <code>!ai &lt;question&gt;</code> on the mesh channel.
</p>
+6 -2
View File
@@ -33,7 +33,7 @@
.mesh-flasher-sep { margin: 0 8px; color: rgba(255, 255, 255, 0.2); }
.mesh-error { color: #ef4444; font-size: 0.85rem; padding: 8px 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.2); flex-shrink: 0; }
.mesh-columns { display: flex; gap: 16px; flex: 1; min-height: 0; overflow: hidden; }
.mesh-left { width: 380px; flex-shrink: 0; display: flex; flex-direction: column; gap: 12px; min-height: 0; overflow-y: auto; }
.mesh-left { width: 380px; flex-shrink: 0; display: flex; flex-direction: column; gap: 12px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.mesh-right { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
.mesh-tools-wrapper { display: contents; }
.mesh-tools-tab-bar { display: none; }
@@ -121,7 +121,7 @@
.mesh-chat-header-sub { font-size: 0.7rem; color: rgba(255, 255, 255, 0.3); font-family: monospace; }
.mesh-chat-header-status { flex-shrink: 0; }
.mesh-chat-header-time { font-size: 0.7rem; color: rgba(255, 255, 255, 0.3); }
.mesh-chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.mesh-chat-messages { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 16px; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.mesh-chat-no-messages { flex: 1; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.25); font-size: 0.85rem; }
.mesh-chat-bubble-wrapper { display: flex; }
.mesh-chat-bubble-wrapper.sent { justify-content: flex-end; }
@@ -232,6 +232,10 @@
.mesh-assistant-field { display: flex; flex-direction: column; gap: 4px; }
.mesh-assistant-install { padding: 12px; background: rgba(251,146,60,0.08); border: 1px solid rgba(251,146,60,0.25); border-radius: 10px; }
.mesh-assistant-install-btn { display: inline-block; text-align: center; padding: 8px 14px; font-size: 0.8rem; }
.mesh-assistant-allowlist { display: flex; flex-direction: column; gap: 2px; max-height: 180px; overflow-y: auto; overscroll-behavior: contain; border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 6px; background: rgba(0,0,0,0.2); }
.mesh-assistant-allow-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; color: rgba(255,255,255,0.85); }
.mesh-assistant-allow-row:hover { background: rgba(255,255,255,0.06); }
.mesh-assistant-allow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mesh-panel-title { font-size: 1rem; font-weight: 700; color: rgba(255,255,255,0.95); margin: 0; }
.mesh-panel-sub { font-size: 0.8rem; color: rgba(255,255,255,0.45); margin: -4px 0 0; }
.mesh-bitcoin-section { display: flex; flex-direction: column; gap: 8px; }
@@ -2,8 +2,6 @@
import InterfaceModeSection from '@/views/settings/InterfaceModeSection.vue'
import ClaudeAuthSection from '@/views/settings/ClaudeAuthSection.vue'
import AIDataAccessSection from '@/views/settings/AIDataAccessSection.vue'
import SystemUpdatesSection from '@/views/settings/SystemUpdatesSection.vue'
import AppRegistriesSection from '@/views/settings/AppRegistriesSection.vue'
import WebhookSection from '@/views/settings/WebhookSection.vue'
import TelemetrySection from '@/views/settings/TelemetrySection.vue'
import BackupSection from '@/views/settings/BackupSection.vue'
@@ -14,8 +12,6 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
<InterfaceModeSection />
<ClaudeAuthSection />
<AIDataAccessSection />
<SystemUpdatesSection />
<AppRegistriesSection />
<WebhookSection />
<TelemetrySection />
<BackupSection />