GXWF_WEB_CACHE_PANEL_PLAN

gxwf-web + gxwf-ui: Tool Cache Debugging Panel

Date: 2026-04-27 Repo: galaxy-tool-util worktree vs_code_integration Depends on: CACHE_ABSTRACTIONS_PLAN.mdToolCache.removeCached, loadCachedRaw, getCacheStats, and CacheStorage.stat have all already shipped (see packages/core/src/cache/tool-cache.ts). No upstream blockers; this plan can land directly. Related but separate: VS_CODE_CACHE_TREE_PLAN.md — that work targets the VS Code extension. This work is for the standalone web app and shares only the upstream ToolCache primitives.


1. Goal

Add a “Tool Cache” tab to the standalone gxwf-ui web app, backed by new gxwf-web API routes that expose the existing state.cache: ToolCache for inspection and management. This is a debugging surface for the cache the server already maintains and shares with anything embedded in the page (including the Monaco-hosted VS Code extension when it’s wired in) — but the panel itself stands alone and depends only on gxwf-web.

Concretely:

2. Non-goals

3. Current shape (orientation)

gxwf-web (packages/gxwf-web/src/):

gxwf-client (packages/gxwf-client/src/index.ts) — thin openapi-fetch wrapper; types come from @galaxy-tool-util/gxwf-web (re-exported from the generated file).

gxwf-ui (packages/gxwf-ui/src/):

4. Architecture

gxwf-web/src/
├── router.ts                    # extend Route union + handler switch
└── tool-cache.ts                # NEW — handler module (mirrors workflows.ts shape)

gxwf-web/openapi.json            # extend with /api/tool-cache/* paths + schemas
gxwf-web/src/generated/          # regenerate via `npm run codegen`

gxwf-ui/src/
├── App.vue                      # add nav link
├── router/index.ts              # add route
├── views/
│   └── ToolCacheView.vue        # NEW
├── components/
│   ├── ToolCacheTable.vue       # NEW — list + filters + per-row actions
│   ├── ToolCacheStats.vue       # NEW — header strip
│   └── ToolCacheRawDialog.vue   # NEW — modal with raw JSON
└── composables/
    └── useToolCache.ts          # NEW — reactive wrapper over the typed client

The new server module is intentionally a sibling of workflows.ts so the router stays small — router.ts only adds Route variants and a single handler block that delegates to tool-cache.ts operations.

AppState change: add infoService: ToolInfoService (resolves the §11 open question — go with the clean version). app.ts already constructs one via makeNodeToolInfoService(...) and discards everything but service.cache; promote the whole service onto state, derive state.cache = state.infoService.cache for back-compat with existing handlers. Plumb the type through index.ts exports.

5. API design

All routes are additive under a new /api/tool-cache prefix. CORS / error handling pass through the existing createRequestHandler machinery.

MethodPathBody / QueryResponse
GET/api/tool-cache{ entries: CachedToolEntry[], stats: CacheStats }
GET/api/tool-cache/statsCacheStats
GET/api/tool-cache/{cacheKey}{ contents: unknown, decodable: boolean } (raw payload)
DELETE/api/tool-cache/{cacheKey}{ removed: boolean }
DELETE/api/tool-cache?prefix=<toolIdPrefix> optional{ removed: number }
POST/api/tool-cache/refetch{ toolId, toolVersion?, toolshedUrl? }{ fetched, alreadyCached, failed[] }
POST/api/tool-cache/add{ toolId, toolVersion? }{ cacheKey, alreadyCached } (diagnostic “populate this id”)

Schemas (added to openapi.json components.schemas):

CachedToolEntry {
  cacheKey: string;
  toolId: string;
  toolVersion: string;
  source: string;          // "api" | "galaxy" | "local" | "orphan" | …
  sourceUrl: string;
  cachedAt: string;        // ISO 8601
  sizeBytes?: number;      // present iff storage.stat is implemented
  decodable: boolean;
  toolshedUrl?: string;    // derived for entries with a parseable ToolShed id
}
CacheStats {
  count: number;
  totalBytes?: number;
  bySource: { [source: string]: number };
  oldest?: string;
  newest?: string;
}

cacheKey in the path is URL-encoded; the upstream cacheKey() function returns hex-style strings so encoding is normally a no-op.

The CachedToolEntry field names above are camelCase, but ToolCache.listCached() / the CacheIndex return snake_case (cache_key, tool_id, tool_version, source_url, cached_at). decorate() does the rename — keep snake_case private to the cache index and present camelCase at the HTTP boundary so the OpenAPI/TS types feel idiomatic on the UI side.

refetch and add both go through state.infoService.getToolInfo(toolId, toolVersion), which already (a) resolves coordinates, (b) tries each configured ToolSource in order, and (c) writes through cache.saveTool. Refetch differs only in that it calls cache.removeCached(cacheKey) first to force a re-pull rather than returning the existing entry. Both responses include alreadyCached so the UI can distinguish a fresh fetch from a no-op.

Implementation in tool-cache.ts

Pure thin wrappers; everything routes through state.cache and is storage-agnostic by virtue of the upstream additions:

export async function listToolCache(state: AppState): Promise<{ entries: CachedToolEntry[]; stats: CacheStats }> {
  const raw = await state.cache.listCached();           // existing
  const stats = await state.cache.getCacheStats();      // new upstream
  const entries = await Promise.all(raw.map(async (e) => decorate(state, e)));
  return { entries, stats };
}

export async function getToolCacheRaw(state: AppState, cacheKey: string) {
  const contents = await state.cache.loadCachedRaw(cacheKey);   // new upstream
  if (contents === null) throw new HttpError(404, `No cached entry: ${cacheKey}`);
  const decodable = canDecode(contents);                        // try ParsedTool decode
  return { contents, decodable };
}

export async function deleteToolCacheEntry(state: AppState, cacheKey: string) {
  const removed = await state.cache.removeCached(cacheKey);     // new upstream
  if (!removed) throw new HttpError(404, `No cached entry: ${cacheKey}`);
  return { removed };
}

export async function clearToolCache(state: AppState, prefix?: string) {
  // Avoid double-listAll: snapshot once, clear, report the snapshot length
  // (clearCache currently removes everything matching, so the count is exact).
  // If we want to be defensive, change upstream `ToolCache.clearCache` to
  // return the removed count; small change worth doing if this ships.
  const before = await state.cache.listCached();
  const matched = prefix === undefined
    ? before
    : before.filter((e) => e.tool_id.startsWith(prefix.replace(/\*$/, "")));
  await state.cache.clearCache(prefix);
  return { removed: matched.length };
}

decorate() adds decodable (cheap try/catch decode) and toolshedUrl (via the existing tool-id parser) on top of the raw index entry.

Router additions (router.ts)

OpenAPI / client regeneration

  1. Edit openapi.json — add the seven paths and two schemas above.
  2. Run pnpm --filter @galaxy-tool-util/gxwf-web codegen (or pnpm codegen from packages/gxwf-web/) — regenerates src/generated/api-types.ts.
  3. gxwf-client re-exports automatically via @galaxy-tool-util/gxwf-web types — pnpm build to typecheck the client.

6. UI: navbar tab and routing

App.vue navbar gains a third internal link, sandwiched before the external IWC link:

<RouterLink to="/" class="nav-link">Workflows</RouterLink>
<RouterLink to="/files" class="nav-link">Files</RouterLink>
<RouterLink to="/cache" class="nav-link">Tool Cache</RouterLink>
<a href="https://iwc.galaxyproject.org/">IWC …</a>

router/index.ts gets a fourth route, lazy-loaded to keep the dashboard bundle lean:

{ path: "/cache", component: () => import("../views/ToolCacheView.vue") },

7. UI: ToolCacheView.vue

Layout:

┌──────────────────────────────────────────────────────────────────────┐
│  Tool Cache                                          [Refresh] [⋯]   │
│                                                                      │
│  ┌── ToolCacheStats ────────────────────────────────────────────┐    │
│  │ 42 tools · 3.1 MB · 38 toolshed · 3 orphan · 1 local         │    │
│  │ Oldest: 2025-12-14   Newest: 2026-04-27                      │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  Filter: [search box]   Source: [All ▾]   ☐ Show only undecodable    │
│                                                                      │
│  ┌── ToolCacheTable ────────────────────────────────────────────┐    │
│  │ ✓  bwa-mem            0.7.17.2  api    87 KB  2 days ago  ⋯  │    │
│  │ ✓  samtools_view      1.15.1    api    52 KB  3 days ago  ⋯  │    │
│  │ ⚠  legacy_tool        1.0       api    11 KB  21 days ago ⋯  │    │
│  │ ⚠  unknown_id         unknown   orph    3 KB    today    ⋯   │    │
│  │ …                                                            │    │
│  └──────────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────────┘

Components:

The title-bar overflow has: Refresh, Add tool… (toolId + optional version → POST /add; primary diagnostic-populate flow), Clear all (with confirm), Clear by prefix… (input dialog).

Re-fetch flow uses an existing toast (primevue/toast, already mounted in App.vue) — success → “Re-fetched bwa-mem 0.7.17.2”, failure → “Failed: ”. Re-fetch always refreshes the table.

Delete shows an inline ConfirmDialog (PrimeVue) — single click deletes if “Don’t ask again this session” is set in the same dialog.

8. UI: useToolCache.ts composable

Mirrors useWorkflows.ts. Reactive entries, stats, loading, error. Methods: refresh, del(cacheKey), clear(prefix?), refetch(toolId, toolVersion), loadRaw(cacheKey). Each method uses the typed client from useApi.ts and re-runs refresh() on mutating success.

9. Tests

10. Rollout order

  1. Land the upstream additions from CACHE_ABSTRACTIONS_PLAN.md. Bump @galaxy-tool-util/core floor in gxwf-web.
  2. gxwf-web: implement tool-cache.ts, extend router.ts. Tests.
  3. Update openapi.json. Run npm run codegen. Verify gxwf-client rebuild typechecks.
  4. gxwf-ui: composable, view, components, router entry, navbar link. Tests.
  5. README touch-up on gxwf-web and gxwf-ui — list the new endpoints / nav tab.

11. Open questions