Merge remote-tracking branch 'gitea-ai/gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf'
Demo images / Build & push demo images (push) Successful in 3m14s

This commit is contained in:
archipelago
2026-08-09 08:17:22 -04:00
481 changed files with 90667 additions and 210 deletions
+65
View File
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { AIContextCategory } from '@/types/aiui-protocol'
import { rpcClient } from '@/api/rpc-client'
const STORAGE_KEY = 'archipelago-ai-permissions'
@@ -97,7 +98,66 @@ export const AI_PERMISSION_CATEGORIES: AIPermissionCategory[] = [
]
export const useAIPermissionsStore = defineStore('aiPermissions', () => {
// Seeded from localStorage so the toggles paint immediately, then reconciled
// with the node in hydrate(). The node is authoritative.
const enabled = ref<Set<AIContextCategory>>(loadFromStorage())
const hydrated = ref(false)
/**
* Reconcile with the node, which is where these grants actually live.
*
* They used to live ONLY in localStorage, which is scoped to an origin — and
* a node answers on several (LAN address, Tailscale address, <host>.local,
* hostname). Granting over one and returning by another showed every switch
* off again: not reset, just never set *there*. Reported as "the AI Data
* Access settings are not persistent through sessions", and it made a working
* content path look broken, because every scope silently returns nothing
* without a grant.
*
* Migration, not replacement: if this browser holds grants and the node holds
* none, the local set is pushed UP rather than being wiped by an empty node.
* That covers everyone who granted permissions before this change — without
* it, upgrading would silently revoke them. The reverse (node has grants,
* browser does not) is the normal case on a new device and the node wins.
*/
async function hydrate(): Promise<void> {
try {
const res = await rpcClient.call<{ granted?: string[] }>({ method: 'ai.permissions.get' })
const remote = new Set(
(res.granted ?? []).filter((c): c is AIContextCategory =>
AI_PERMISSION_CATEGORIES.some(cat => cat.id === c),
),
)
if (remote.size === 0 && enabled.value.size > 0) {
await pushToNode()
} else {
enabled.value = remote
save()
}
} catch (e) {
// Offline, or a node too old to know the method: keep whatever
// localStorage had. Degrading to the old behaviour is strictly better
// than blanking the operator's grants because a request failed.
if (import.meta.env.DEV) console.warn('AI permissions: node unreachable, using local', e)
} finally {
hydrated.value = true
}
}
async function pushToNode(): Promise<void> {
try {
await rpcClient.call({
method: 'ai.permissions.set',
params: { granted: [...enabled.value] },
})
} catch (e) {
// The local write already happened, so the UI stays consistent with what
// the operator just clicked; it will be pushed again on the next change
// or the next hydrate().
if (import.meta.env.DEV) console.warn('AI permissions: failed to persist to node', e)
}
}
function loadFromStorage(): Set<AIContextCategory> {
try {
@@ -130,16 +190,19 @@ export const useAIPermissionsStore = defineStore('aiPermissions', () => {
// Trigger reactivity
enabled.value = new Set(enabled.value)
save()
void pushToNode()
}
function enableAll() {
enabled.value = new Set(AI_PERMISSION_CATEGORIES.map(c => c.id))
save()
void pushToNode()
}
function disableAll() {
enabled.value = new Set()
save()
void pushToNode()
}
const enabledCategories = computed(() => [...enabled.value])
@@ -148,6 +211,8 @@ export const useAIPermissionsStore = defineStore('aiPermissions', () => {
return {
enabled,
hydrated,
hydrate,
isEnabled,
toggle,
enableAll,