Skip to content

Commit 464a555

Browse files
committed
feat(skill): implement cross-WebUI skill registry synchronization to ensure up-to-date skill availability in DaoAgentUI
1 parent 08bb2e3 commit 464a555

3 files changed

Lines changed: 79 additions & 11 deletions

File tree

src/dao/browser/ui/webui/resources/agent/dao_agent_app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import './dao_settings_view.js';
1717
import type {DaoChatView} from './dao_chat_view.js';
1818
import type {DaoSettingsView} from './dao_settings_view.js';
1919
import {initI18n} from './i18n/i18n.js';
20+
import {refreshSkillRegistryIfStale} from './skill_registry.js';
2021

2122
// Kick off locale loading at module import time so the dictionary is in
2223
// place before any t() call from a child view's render. Fire-and-forget:
@@ -62,6 +63,10 @@ export class DaoAgentApp extends CrLitElement {
6263
document.addEventListener('visibilitychange', () => {
6364
if (document.visibilityState === 'visible') {
6465
setTimeout(() => this.getChatView_()?.focusInput(), 100);
66+
// Cross-WebUI sync: skills created in the dao://skills tab while
67+
// this sidebar was hidden won't have propagated to our cache.
68+
// Throttled — no-op when the registry is already fresh.
69+
void refreshSkillRegistryIfStale();
6570
}
6671
});
6772
if (document.visibilityState === 'visible') {

src/dao/browser/ui/webui/resources/agent/dao_chat_view.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {buildAgentTools} from './pi_tool_adapter.js';
3333
import {toolConfigChannel} from './tool_catalog.js';
3434
import './dao_chat_history_panel.js';
3535
import {ensurePiAppStorage, syncActiveKeyToPiStorage} from './pi_app_storage.js';
36-
import {getAllSkills, initSkillRegistry, loadSkillInstructions, type SkillRegistryEntry} from './skill_registry.js';
36+
import {getAllSkills, initSkillRegistry, loadSkillInstructions, refreshSkillRegistry, refreshSkillRegistryIfStale, type SkillRegistryEntry} from './skill_registry.js';
3737
import {t} from './i18n/i18n.js';
3838
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3939
import * as pi from './vendor/pi_runtime_bundle.js';
@@ -1870,6 +1870,19 @@ export class DaoChatView extends CrLitElement {
18701870
this.hideSkillPicker_();
18711871
return;
18721872
}
1873+
// Cross-WebUI sync: skills created/deleted in the dao://skills tab
1874+
// don't update this page's module-local registry cache. Refresh on
1875+
// demand (throttled to avoid spamming on every keystroke) and re-run
1876+
// the picker once new data lands so newly-added skills show up
1877+
// without a sidebar reload.
1878+
void refreshSkillRegistryIfStale().then((refreshed) => {
1879+
if (!refreshed) return;
1880+
const ta2 = this.composerTextarea_;
1881+
if (!ta2) return;
1882+
if (/^\/([A-Za-z0-9_-]*)$/.test(ta2.value)) {
1883+
this.updateSkillPicker_();
1884+
}
1885+
});
18731886
this.skillPickerQuery_ = (m[1] || '').toLowerCase();
18741887
const all = getAllSkills();
18751888
const q = this.skillPickerQuery_;
@@ -2002,8 +2015,20 @@ export class DaoChatView extends CrLitElement {
20022015
// Warm the instructions cache before the message hits state.messages so
20032016
// convertToLlm can splice the body in synchronously on the next turn.
20042017
private async ensureSkillLoadedFromText_(text: string): Promise<void> {
2005-
const parsed = this.parseSkillPrefix_(text);
2006-
if (!parsed) return;
2018+
let parsed = this.parseSkillPrefix_(text);
2019+
if (!parsed) {
2020+
// The text looks like `/<id> ...` but the id wasn't in our cache.
2021+
// This is the "user created a skill in dao://skills and typed
2022+
// /id fast enough to skip the picker" path. Bypass the staleness
2023+
// throttle (a recent refresh from picker-open could still be from
2024+
// before the skill was created in the other tab) and force one
2025+
// fresh fetch before declaring the prefix unknown.
2026+
if (/^\/([A-Za-z0-9_-]+)(?:\s|$)/.test(text.trim())) {
2027+
await refreshSkillRegistry();
2028+
parsed = this.parseSkillPrefix_(text);
2029+
}
2030+
if (!parsed) return;
2031+
}
20072032
if (this.skillInstructionsCache_.has(parsed.skillId)) return;
20082033
const content = await loadSkillInstructions(parsed.skillId);
20092034
if (content && content.instructions) {

src/dao/browser/ui/webui/resources/agent/skill_registry.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,58 @@ export interface SkillContent {
2121
}
2222

2323
// ---- Cached Registry ----
24+
//
25+
// The registry is module-local and therefore per-WebUI: dao://agent and
26+
// dao://skills each load their own copy of this file with its own
27+
// `cachedRegistry`. That means `saveUserSkill` writing to disk from one
28+
// page does NOT propagate to the other page's cache. Callers in
29+
// dao://agent must call `refreshSkillRegistryIfStale` (or
30+
// `refreshSkillRegistry`) on UI signals where the registry might have
31+
// changed in another tab — most importantly when the slash picker is
32+
// opened. Without that, a skill created in dao://skills won't be
33+
// invokable as `/<id>` until the sidebar reloads.
2434

2535
let cachedRegistry: SkillRegistryEntry[] = [];
36+
let lastRefreshAt_ = 0;
37+
let inflightRefresh_: Promise<void>|null = null;
2638

27-
export async function initSkillRegistry(): Promise<void> {
28-
try {
29-
const result = await callNative('getSkillRegistry') as SkillRegistryEntry[];
30-
cachedRegistry = result || [];
31-
} catch (_) {
32-
cachedRegistry = [];
33-
}
39+
export function initSkillRegistry(): Promise<void> {
40+
if (inflightRefresh_) return inflightRefresh_;
41+
inflightRefresh_ = (async () => {
42+
try {
43+
const result =
44+
await callNative('getSkillRegistry') as SkillRegistryEntry[];
45+
cachedRegistry = result || [];
46+
} catch (_) {
47+
cachedRegistry = [];
48+
} finally {
49+
lastRefreshAt_ = Date.now();
50+
inflightRefresh_ = null;
51+
}
52+
})();
53+
return inflightRefresh_;
54+
}
55+
56+
export function refreshSkillRegistry(): Promise<void> {
57+
return initSkillRegistry();
3458
}
3559

36-
export async function refreshSkillRegistry(): Promise<void> {
60+
// On-demand cross-WebUI sync. Refreshes the cache from disk only when the
61+
// last fetch was older than `maxAgeMs`, or when another refresh is
62+
// already in flight (in which case it just awaits it). Returns true when
63+
// a real refresh actually ran (so the caller knows the cache may have
64+
// changed and any dependent UI should re-render); false when throttled.
65+
export async function refreshSkillRegistryIfStale(
66+
maxAgeMs = 1500): Promise<boolean> {
67+
if (inflightRefresh_) {
68+
await inflightRefresh_;
69+
return true;
70+
}
71+
if (lastRefreshAt_ !== 0 && Date.now() - lastRefreshAt_ < maxAgeMs) {
72+
return false;
73+
}
3774
await initSkillRegistry();
75+
return true;
3876
}
3977

4078
export function getAllSkills(): SkillRegistryEntry[] {

0 commit comments

Comments
 (0)