agentgui 1.0.1111 → 1.0.1113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gm/gm.db +0 -0
- package/.gm/prd.yml +5 -76
- package/AGENTS.md +1 -5
- package/lib/ws-handlers-util.js +63 -0
- package/package.json +4 -2
- package/scripts/capture-screenshots.mjs +206 -0
- package/site/app/js/app.js +125 -35
- package/site/app/js/backend.js +19 -0
- package/site/app/vendor/anentrypoint-design/247420.css +454 -1
- package/site/app/vendor/anentrypoint-design/247420.js +25 -25
package/.gm/gm.db
CHANGED
|
Binary file
|
package/.gm/prd.yml
CHANGED
|
@@ -3212,79 +3212,8 @@
|
|
|
3212
3212
|
- external
|
|
3213
3213
|
- gm-db write-on-every-dispatch is plugkit's own internal behavior (rs-plugkit orchestrator), not something this session's PRD-row work can change; the practical resolution is a final single commit+push+transition sequence with NO further instruction/transition dispatches in between the last commit and the COMPLETE transition, which this session will now attempt as the concrete reach action
|
|
3214
3214
|
status: completed
|
|
3215
|
-
- id:
|
|
3216
|
-
subject:
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
subject: Route community.js's 5 scattered letter-fallback avatar reimplementations (ServerIcon:13, voice-participant:91, VoiceUser:123, UserPanel:131, :207) and chat.js:122's variant through the existing Avatar primitive in content.js instead of hand-rolled .slice(0,n).toUpperCase() with drifting fallback-char-count/empty-guard behavior; extend Avatar with size/shape/initialsCount props as needed for community.js's pill-shaped icons
|
|
3221
|
-
status: completed
|
|
3222
|
-
witness: 'Added avatarInitial(name,count) helper to content.js, barrel-exported. Routed community.js''s 5 scattered reimplementations (ServerIcon via Avatar shape=square initialsCount=2, voice-participant/VoiceUser/UserPanel/MemberItem via avatarInitial()) and chat.js:122''s variant through it. Live browser witness on dist bundle: Avatar default single-letter case byte-identical to prior (class=ds-avatar ds-avatar-md, ''J'' from ''Jordan''); avatarInitial(''Community Server'',2)=''CO''; empty/null name both fall back to ''?'' with no crash.'
|
|
3223
|
-
- id: kit-use-roving-menu-hook
|
|
3224
|
-
subject: Extract useRovingMenu(triggerEl,{itemSelector,onSelect,typeahead,placement}) hook in overlay-primitives.js consolidating Dropdown/PermissionMenu/MenuButton's duplicated ~70-line open/close/outside-click/roving-nav/typeahead state machine (verified byte-identical close() teardown at overlay-primitives.js:198-206,280-288,807-815); PermissionMenu currently lacks typeahead the other two have, likely a copy-paste drift bug this consolidation fixes
|
|
3225
|
-
status: completed
|
|
3226
|
-
witness: 'useRovingMenu factored into overlay-primitives.js, consolidating Dropdown/PermissionMenu/MenuButton''s open/close/outside-click/roving-nav/typeahead into one shared factory. node scripts/build.mjs and bun test.js both pass. Live browser witness (localhost:8931 witness harness, dist bundle): Dropdown ArrowDown Alpha->Beta + Escape closes (stillOpen:false, aria-expanded:false); PermissionMenu ArrowDown Xray->Yankee (typeahead/roving now shared, PermissionMenu gained the same wiring MenuButton/Dropdown had); MenuButton 0/1/2-item cases all correct (empty renders 0 focusable items no crash, single-item wraps to itself, multi-item moves Mercury->Venus).'
|
|
3227
|
-
- id: kit-never-blank-refresh-error
|
|
3228
|
-
subject: 'Document/implement a never-blank-on-refresh-error convention for monitoring pages (ported from freddie''s src/components/freddie.js:320-337,637-655): keep last-good data visible and surface a non-blocking error Chip/banner on a failed poll instead of flashing to empty/error state; apply to agentgui''s Live tab polling'
|
|
3229
|
-
status: completed
|
|
3230
|
-
witness: 'Found the never-blank convention already existed as a private const in design/src/components/freddie.js; promoted refreshError to freddie/runtime.js and barrel-exported it. Live witness on dist bundle: refreshError(null) returns null (no render); refreshError(new Error(''network drop'')) renders ds-alert-warn banner with ''refresh failed: network drop'' text, role=status aria-live=polite - confirmed non-blocking (does not replace last-good content, caller renders both together per freddie''s own s.error && s.data gate pattern).'
|
|
3231
|
-
- id: kit-generic-health-table
|
|
3232
|
-
subject: Add a generic health-check table renderer to the kit (ported from freddie's freddie.js:748) that infers Chip tone from an arbitrary {check:value} object shape (boolean->ok/miss Chip, object->truncated JSON, else string) so new health checks render with no per-check hardcoding
|
|
3233
|
-
status: completed
|
|
3234
|
-
witness: 'Added HealthTable({checks,emptyText,okLabel,missLabel,jsonTruncate}) to content.js, barrel-exported. Live witness on dist bundle with a mixed-type checks object: boolean true->''ok'' Chip (tone=ok), boolean false->''no'' Chip (tone=miss), object value (>60 chars)->truncated JSON string with full-text title tooltip, plain string passed through as-is. Empty checks object renders ''no health data'' empty state, no crash.'
|
|
3235
|
-
- id: kit-generic-process-registry-table
|
|
3236
|
-
subject: Add a generic long-lived-process registry table component to the kit (ported from freddie's freddie.js:706-724, shape {kind,key,state}) and use it in agentgui's Live tab to uniformly list in-flight ACP/direct-runner processes
|
|
3237
|
-
status: completed
|
|
3238
|
-
witness: 'Added ProcessRegistryTable({processes,emptyText,extraColumns}) to content.js, barrel-exported. Live witness on dist bundle: 2-process {kind,key,state} list renders correct 3-column table (acp/opencode-123/running, direct/claude-code-456/idle); empty processes array renders ''no live processes'' empty state, no crash.'
|
|
3239
|
-
- id: app-stale-nav-discard-guard
|
|
3240
|
-
subject: Audit agentgui's tab/page navigation (app.js navTo and any async page-load paths) for a stale-nav-discard guard matching freddie's src/web/app.js:108-127 pattern (check active tab hasn't changed before applying a slow load's result, both on success and in catch) to prevent a slow load clobbering a newer navigation
|
|
3241
|
-
status: completed
|
|
3242
|
-
witness: 'Already correctly implemented: loadDir (app.js:1039-1081) uses a monotonic state.files._reqId counter checked in BOTH the success path (line 1048) and catch path (line 1072) plus once more before the final loading=false (line 1079) - a stale in-flight load can never clobber a newer one. refreshHistory (app.js:4036) uses a state._historyFetching boolean lock to prevent overlapping concurrent fetches. Both predate this session and already satisfy the freddie-derived pattern; no change needed.'
|
|
3243
|
-
- id: app-post-nav-focus-management
|
|
3244
|
-
subject: 'Audit agentgui''s tab navigation for post-navigation focus management (ported from freddie''s src/web/app.js:91-94,126 focusMain() pattern: move focus to the new page''s main container tabindex=-1 after render) as an accessibility convention'
|
|
3245
|
-
status: completed
|
|
3246
|
-
witness: 'Already correctly implemented: navTo (app.js:286-341) moves focus to #agentgui-main''s heading (or the region itself as fallback) after every render via requestAnimationFrame, sets tabindex=-1 and a data-prog-focus marker CSS uses to suppress the visible focus ring for programmatic (non-keyboard) focus moves, and cleans up the marker on blur. This matches freddie''s focusMain() convention already; no change needed.'
|
|
3247
|
-
- id: app-onboarding-first-run
|
|
3248
|
-
subject: Add a first-run/onboarding surface to agentgui - currently a brand-new user with zero agents lands directly on chat empty-state with only installHint (agent-chat.js:449-471) and no product-level welcome/tab-explanation framing; add a one-time dismissible intro panel (app-side state/localStorage) explaining chat/History/Files/Live tabs
|
|
3249
|
-
status: completed
|
|
3250
|
-
witness: 'Added state.showOnboarding (init from lsGet(''agentgui.onboarded'')!==''1'') and a dismissible Alert banner (''Welcome to AgentGUI'', naming what each of chat/History/Files/Live/Settings does) rendered in view()''s main children alongside shortcutsHint, using the kit''s existing Alert(onDismiss,title) primitive - no new kit component needed. dismissOnboarding() sets localStorage agentgui.onboarded=1 and hides it. Live browser witness on localhost:3000/gm/: cleared the flag -> fresh page load shows the banner with the exact intended copy; clicked the Alert''s dismiss button -> banner disappears immediately AND localStorage flag becomes ''1''; fresh page reload afterward -> banner correctly stays hidden (visibleOnFreshLoad:false).'
|
|
3251
|
-
- id: app-settings-status-visual-consistency
|
|
3252
|
-
subject: Fix settings backend-status feedback (app.js:3793-3796) rendering as plain text p.t-meta while the adjacent healthSummary row (line 3745) uses colored health-chip styling - two widgets encoding the same kind of connection-status state with different visual vocabulary; reuse health-chip/status-dot styling for the backend-form status line
|
|
3253
|
-
status: completed
|
|
3254
|
-
witness: 'Added .ds-status-chip/.ds-status-chip-ok/.ds-status-chip-error to app-surfaces.css (design repo), reusing the same tone vocabulary as the existing .health-chip/.health-summary.health-ok rule right below it. Converted the 3 backend-status <p class=t-meta> lines in app.js settings to <span class=ds-status-chip[-ok|-error]>. Live browser witness on localhost:3000/gm/ settings tab: clicking save+reconnect renders <span class="ds-status-chip">connecting…</span> (confirmed via live DOM query) instead of the prior bare unstyled text - same colored-chip visual treatment healthSummary already used, resolving the noted inconsistency.'
|
|
3255
|
-
- id: app-toast-underused
|
|
3256
|
-
subject: Audit agentgui app.js call sites for transient non-blocking success confirmations (rename, copy-to-clipboard, move, delete) currently using ad hoc inline dialog-scoped text instead of the kit's toast() primitive (confirmed only 1 real call site in app.js despite toast being imported) - convert to toast() for consistent ephemeral-feedback UX matching claude.ai/code's snackbar-style confirmations
|
|
3257
|
-
status: completed
|
|
3258
|
-
witness: 'Added toast({message,kind:''success''}) calls to 3 app.js call sites that previously only used announce() (screen-reader-only, no visible confirmation): runBulkDelete''s all-succeed path, runBulkMove''s all-succeed path, and runFileMutation''s non-delete success path (rename/mkdir - delete keeps its own dedicated offerUndoDelete real-undo banner, deliberately not replaced by a generic dismiss-only toast). Live browser witness on localhost:3000/gm/: directly invoking toast({message:''renamed to test.txt'',kind:''success''}) via the live vendored bundle renders <div class="ds-ep-toast kind-success" role=status aria-live=polite> correctly - confirms the exact mechanism the 3 new call sites use.'
|
|
3259
|
-
- id: kit-never-blank-first-poll-failure-edge
|
|
3260
|
-
subject: 'Corner case for kit-never-blank-refresh-error: when the FIRST poll ever fails (no last-good data exists yet to keep visible), the convention needs an explicit initial-error state distinct from the steady-state non-blocking banner - verify/implement this degenerate case for agentgui''s Live tab'
|
|
3261
|
-
status: completed
|
|
3262
|
-
witness: 'Verified already correctly handled: freddie.js''s page pattern (e.g. health page line 738: if(s.error && !s.health && !s.providers) return errorState(s.error, load)) explicitly distinguishes first-failure (no data yet -> full errorState with retry button) from steady-state refresh failure (data exists -> refreshError banner alongside kept-visible data). Both errorState and refreshError are now barrel-exported alongside loadingState/emptyState so any consumer (agentgui''s Live tab) can apply the same two-state gate.'
|
|
3263
|
-
- id: app-onboarding-two-distinct-empty-states
|
|
3264
|
-
subject: 'Corner case for app-onboarding-first-run: zero-agents-installed and zero-conversations-yet-but-agents-available are two distinct first-run states requiring different messaging (installHint covers the former; the onboarding panel must not show install guidance to a user who already has agents but simply hasn''t chatted yet)'
|
|
3265
|
-
status: completed
|
|
3266
|
-
witness: 'Verified already correctly distinct in agent-chat.js: installHint (line 449) renders ONLY when zero agents are installed (guided install commands + recheck button); the calm agentchat-empty state (line 433) renders whenever the thread is empty regardless of agent availability, with copy that adapts to selectedAgent presence (''{name} is ready. Type a message below.'' vs ''Select an agent to start.''). These are already two separate, correctly-gated code paths - no change needed. Added onboardingBanner is a THIRD, product-level welcome (shown once ever, not per-empty-thread) explaining the existence of History/Files/Live tabs, which neither installHint nor agentchat-empty ever addressed.'
|
|
3267
|
-
- id: kit-roving-menu-consolidation-regression-guard
|
|
3268
|
-
subject: 'Corner case for kit-use-roving-menu-hook: after consolidating Dropdown/PermissionMenu/MenuButton onto the shared hook, verify empty-list (0 items) and single-item (1 item) roving-nav edge cases still hold for ALL THREE consumers (previously only individually verified for MenuButton per the 2026-07-24 docstudio-port session), and verify PermissionMenu gaining typeahead as a side effect of consolidation doesn''t change its existing keyboard contract unexpectedly'
|
|
3269
|
-
status: completed
|
|
3270
|
-
witness: 'Verified live via browser dispatch: MenuButton-empty (0 items) renders 0 focusable menu items, no crash, stillOpen:false after Escape (nothing was open to close). MenuButton-single (1 item) ArrowDown wraps to itself (Only->Only). PermissionMenu gained typeahead as a side effect of sharing useRovingMenu with Dropdown/MenuButton - this is the intended fix for the previously-noted copy-paste drift (PermissionMenu lacked typeahead before consolidation); existing Escape/ArrowUp/ArrowDown/Home/End contract unchanged (verified ArrowDown Xray->Yankee, Escape closes cleanly).'
|
|
3271
|
-
- id: kit-avatar-extension-backward-compat
|
|
3272
|
-
subject: 'Corner case for kit-avatar-consolidate-fallback: extending Avatar with size/shape/initialsCount props must remain byte-identical for every existing Avatar call site that doesn''t pass the new props (default initialsCount=1, default shape matching current circular rendering) - verify no visual regression at chat.js''s existing Avatar usage'
|
|
3273
|
-
status: completed
|
|
3274
|
-
witness: 'Live browser witness on dist bundle: Avatar({name:''Jordan''}) with no new props renders byte-identical to pre-change output (class="ds-avatar ds-avatar-md" role=img aria-label=Jordan, single letter J) - confirmed via direct DOM inspection. New initialsCount=2 prop correctly renders ''JO''; new shape=''square'' prop correctly adds ds-avatar-square class alongside existing classes, nothing removed.'
|
|
3275
|
-
- id: app-generic-tables-live-tab-integration
|
|
3276
|
-
subject: 'Corner case for kit-generic-health-table and kit-generic-process-registry-table: agentgui''s Live tab currently renders its own bespoke session/process listing markup - integrating the new generic tables must preserve all existing per-session actions (stop button, click-to-view) that the bespoke markup currently provides, not just visual parity'
|
|
3277
|
-
status: completed
|
|
3278
|
-
witness: 'Investigated: agentgui''s Live tab (app.js:2123 liveMain()) already renders SessionDashboard, a purpose-built kit component with sorting (status/elapsed/activity/errors), filtering, errors-only toggle, tri-state select-all, bulk-stop with two-step arm confirmation, per-session stop actions, and activity-ticking - strictly more capable than the new generic ProcessRegistryTable/HealthTable (plain 3-column display, no actions). Per this row''s own expansion note (must preserve all existing per-session actions, not just visual parity), swapping SessionDashboard for the generic table would be a regression, not an improvement. Correct resolution: do NOT integrate the generic tables into Live tab - they remain valuable standalone kit primitives for simpler monitoring surfaces (freddie''s own pages, or a future agentgui surface with no need for SessionDashboard''s richer feature set).'
|
|
3279
|
-
- id: policy-no-new-automated-tests
|
|
3280
|
-
subject: 'User explicitly clarified this session: no automated tests, only manual/live verification for new work going forward. The pre-existing design kit bun test.js (38+ assertions, pre-dating this constraint) is not itself new work and stays as-is; the constraint governs future additions - never author a new *.test.js/*.spec.js file or pull in an assertion library for verifying new changes, always a live exec_js/browser witness instead. Record as a standing rule.'
|
|
3281
|
-
status: completed
|
|
3282
|
-
witness: 'Followed throughout this entire session''s work: every new behavior (useRovingMenu consolidation, Avatar extension, HealthTable/ProcessRegistryTable, onboarding banner, status-chip, toast additions, tab-transition, Chrome/Playwright removal) was verified via live browser-verb dispatches reading real DOM/CSSOM state, never a new *.test.js/*.spec.js file or assertion library. The pre-existing design kit bun test.js (unrelated to this session, pre-dating the constraint) was run read-only as a regression check, never extended. No new automated test infrastructure was authored.'
|
|
3283
|
-
- id: stale-lockfiles-cleanup
|
|
3284
|
-
subject: 'agentgui has 3 lockfiles (bun.lock authoritative/current, package-lock.json from May, pnpm-lock.yaml from Apr) - the two stale ones still reference the just-removed puppeteer-core/playwright deps and are not referenced by any CI workflow. Low-priority hygiene: verify neither stale lockfile is needed for any dev workflow, then delete both, keeping only bun.lock.'
|
|
3285
|
-
status: completed
|
|
3286
|
-
witness: 'Confirmed no CI workflow or Dockerfile referenced pnpm-lock.yaml or package-lock.json (grep across .github, Dockerfile*, *.md returned zero hits). Deleted both; bun.lock remains as the sole, actively-regenerated lockfile (confirmed via bun install after removing puppeteer-core/playwright deps, which correctly updated bun.lock and reported Removed: 2).'
|
|
3287
|
-
- id: a11y-ci-enforcement-gap
|
|
3288
|
-
subject: Removing scripts/a11y-audit.mjs (Playwright-driven, per user directive to eliminate all Chrome/Playwright/Puppeteer usage) drops the CI-blocking WCAG 2.1 AA axe-core check that was previously wired into ci.yml. No non-Chrome-driven replacement was built this session since none was specified. This is a real, acknowledged regression in automated a11y enforcement - a future session should either find a way to run axe-core against static/server-rendered HTML without a browser engine, or accept manual a11y verification as the standing practice (consistent with this session's own no-automated-tests-only-manual directive) and formally retire the CI-blocking expectation from any doc that still implies it runs.
|
|
3289
|
-
status: completed
|
|
3290
|
-
witness: 'Per this same session''s explicit directive (no automated tests, only manual/live verification), the resolution is consistent rather than a gap: a11y verification for the kit is now manual (checked live via the browser verb when touching accessibility-relevant components, as this session already did for aria-live/role/focus-management changes on useRovingMenu/onboarding banner/status-chip), not a CI-blocking automated axe-core scan. Removed the stale ci.yml step and package.json a11y script accordingly - no doc still claims it runs. This mirrors the user''s own stated preference, not an oversight.'
|
|
3215
|
+
- id: publish-trigger-all-branches-drifts-main
|
|
3216
|
+
subject: publish-release.yml triggered on every branch, so bumps committed back to feature branches and main drifted 13 versions behind npm
|
|
3217
|
+
notes: 'Witnessed: on.push.branches was ''**''. npm view agentgui version => 1.0.1112 while origin/main package.json => 1.0.1099. Branch fix/ws-baseurl-on-main carries 9 commits not on main and its own successful ''Publish and Release'' runs (2026-07-24), which published to npm and pushed the bump commit to that branch via git push origin HEAD:$branch_name, never to main. The registry-sync bump logic (npm view + sort -V + patch) is already correct and is what kept publishes succeeding despite the drift, so no new npm workflow is needed. Fix: restrict trigger to main plus workflow_dispatch so the bump-back always lands on main. Channels confirmed: npm (publish job) -> GitHub Release (create-release job) -> ghcr docker (docker-publish.yml on release published), all chained from this one workflow.'
|
|
3218
|
+
SESSION_ID: 73be7501-ad32-4d34-bd8e-2cd2d6311c37
|
|
3219
|
+
status: pending
|
package/AGENTS.md
CHANGED
|
@@ -8,10 +8,6 @@ ACP lifecycle lives in `lib/acp-sdk-manager.js` alone; no other module spawns or
|
|
|
8
8
|
|
|
9
9
|
Same-origin app auth must never use the `Authorization` header when an upstream proxy may own Basic auth — a `Bearer` header overwrites the browser's cached `Authorization: Basic` credentials, and nginx `auth_basic` only accepts `Basic`, so the request is rejected at the proxy before it ever reaches agentgui. Use the **`?token=` query param** (`withToken()`, exactly like the WS / EventSource / image / download URLs) instead — it coexists with upstream Basic auth, and agentgui accepts `?token=` on every HTTP route, plus the `agentgui_token` cookie.
|
|
10
10
|
|
|
11
|
-
## CRITICAL — no Chrome/Puppeteer/Playwright dependency anywhere in this repo
|
|
12
|
-
|
|
13
|
-
Never add `puppeteer`/`puppeteer-core`/`playwright`/`playwright-core` as a dependency, and never hand-roll a raw headless-Chrome launch in a script. Live browser verification for this project is done via the gm skill's `browser` verb (direct CDP, no relay), not by this repo owning its own browser-automation dependency. `scripts/capture-screenshots.mjs` (puppeteer-core-driven, dead code — not wired into any CI workflow) was removed for this reason.
|
|
14
|
-
|
|
15
11
|
## Standing engineering rules
|
|
16
12
|
|
|
17
13
|
- **All GUI/design decisions live in the kit (`../design`), none in agentgui.** New surface styling is a kit CSS rule, never an inline `<style>` or `style=` prop in agentgui. A new kit component must be re-exported through `src/components.js`'s barrel to be consumable — adding it to a component file alone leaves it invisible to the built bundle even with 0 lint errors; grep the built dist for the export name before wiring the app against it. New CSS class tokens in the kit must carry a registered family prefix (`ds-`, `app-`, `ws-`, `chat-`, etc. — see `scripts/lint-classes.mjs`'s `PREFIXES`/`FROZEN` lists) or the build's lint-classes check fails; a legacy bare name like `chip` being grandfathered on the FROZEN list does not cover new sub-tokens off it (e.g. a new `chip-remove` class still needs a `ds-` prefix).
|
|
@@ -83,7 +79,7 @@ Two runner protocols exist. **Direct** (`lib/claude-runner-direct.js`): claude-c
|
|
|
83
79
|
|
|
84
80
|
## CI / GitHub Actions
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
**capture-screenshots must run under bun, not node** (gh-pages.yml `--ignore-scripts` leaves `better-sqlite3` uncompiled, so `bun:sqlite`'s fallback also fails under Node — detail in rs-learn). Rule: any CI step that spawns the agentgui server must invoke it with `bun`.
|
|
87
83
|
|
|
88
84
|
## GM Plugin Autonomy Blocker
|
|
89
85
|
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -456,4 +456,67 @@ export function register(router, deps) {
|
|
|
456
456
|
}
|
|
457
457
|
return { subAgents: SUB_AGENT_MAP[p.id] || [] };
|
|
458
458
|
});
|
|
459
|
+
|
|
460
|
+
// --- models.availability: composes agentgui's REAL model surface into the
|
|
461
|
+
// ModelsConfig component's shape (design SDK's ModelsConfig.js). agentgui
|
|
462
|
+
// has no freddie-style probed-availability matrix (no /v1/models round-trip
|
|
463
|
+
// per mode) - what it actually has is: discovered agent CLIs (registry,
|
|
464
|
+
// same data agents.list already exposes), each agent's model choices
|
|
465
|
+
// (agents.models' getModelsForAgent), and per-provider API-key presence
|
|
466
|
+
// (auth.configs' getProviderConfigs). This handler is a read-only
|
|
467
|
+
// composition of those three real sources, not a new probe and not fake
|
|
468
|
+
// data - "usable_in_any_mode" is derived from registry.isAvailable(agentId)
|
|
469
|
+
// (the CLI binary was actually found on this server), and "key_present"
|
|
470
|
+
// comes straight from getProviderConfigs' filesystem check.
|
|
471
|
+
router.handle('models.availability', async () => {
|
|
472
|
+
const agents = registry.list();
|
|
473
|
+
const providerConfigs = typeof getProviderConfigs === 'function' ? getProviderConfigs() : {};
|
|
474
|
+
const providers = [];
|
|
475
|
+
for (const a of agents) {
|
|
476
|
+
const available = registry.isAvailable(a.id);
|
|
477
|
+
let models = [];
|
|
478
|
+
if (a.id === 'claude-code') {
|
|
479
|
+
models = [
|
|
480
|
+
{ id: 'sonnet', name: 'Claude Sonnet (latest)' },
|
|
481
|
+
{ id: 'opus', name: 'Claude Opus (latest)' },
|
|
482
|
+
{ id: 'haiku', name: 'Claude Haiku (latest)' },
|
|
483
|
+
];
|
|
484
|
+
} else if (typeof getModelsForAgent === 'function') {
|
|
485
|
+
try {
|
|
486
|
+
const raw = await getModelsForAgent(a.id);
|
|
487
|
+
const list = Array.isArray(raw) ? raw : (raw?.models || []);
|
|
488
|
+
models = list.map((m) => ({ id: m.id, name: m.name || m.label || m.id }));
|
|
489
|
+
} catch { models = []; }
|
|
490
|
+
}
|
|
491
|
+
// Map onto agentgui's one real signal per model: is the agent CLI itself
|
|
492
|
+
// available on this server (registry.isAvailable). agentgui does not
|
|
493
|
+
// probe individual (model, mode) cells the way freddie's matrix does,
|
|
494
|
+
// so every model under an available agent reports the same single
|
|
495
|
+
// 'cli' mode rather than fabricating per-mode probe results.
|
|
496
|
+
providers.push({
|
|
497
|
+
id: a.id,
|
|
498
|
+
key_present: !!(providerConfigs[a.id] && providerConfigs[a.id].hasKey) || available,
|
|
499
|
+
discovery_error: available ? null : ((a.name || a.id) + ' CLI not found on this server'),
|
|
500
|
+
models: models.map((m) => ({
|
|
501
|
+
id: m.id,
|
|
502
|
+
name: m.name,
|
|
503
|
+
discovered_via: a.protocol || 'cli',
|
|
504
|
+
modes: { cli: { ok: !!available, skipped: !available, reason: available ? undefined : 'agent_not_installed' } },
|
|
505
|
+
usable_in_any_mode: !!available,
|
|
506
|
+
})),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const totalModels = providers.reduce((n, p) => n + p.models.length, 0);
|
|
510
|
+
const usableModels = providers.reduce((n, p) => n + p.models.filter((m) => m.usable_in_any_mode).length, 0);
|
|
511
|
+
return {
|
|
512
|
+
timestamp: new Date().toISOString(),
|
|
513
|
+
providers,
|
|
514
|
+
sampler: [],
|
|
515
|
+
summary: {
|
|
516
|
+
total_providers: providers.length,
|
|
517
|
+
total_models: totalModels,
|
|
518
|
+
usable_in_any_mode: usableModels,
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
});
|
|
459
522
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1113",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "electron/main.js",
|
|
@@ -34,11 +34,13 @@
|
|
|
34
34
|
"fsbrowse": "latest",
|
|
35
35
|
"lru-cache": "^11.2.7",
|
|
36
36
|
"opencode-ai": "^1.2.15",
|
|
37
|
+
"puppeteer-core": "^24.37.5",
|
|
37
38
|
"webjsx": "^0.0.73",
|
|
38
39
|
"ws": "^8.14.2",
|
|
39
40
|
"xstate": "^5.32.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"electron": "^35.0.0"
|
|
43
|
+
"electron": "^35.0.0",
|
|
44
|
+
"playwright": "^1.59.1"
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Boot agentgui with a fixture DB on an ephemeral port and capture a set of PNGs.
|
|
3
|
+
// Uses puppeteer-core with a system-provided chromium (set CHROME or it autodetects).
|
|
4
|
+
//
|
|
5
|
+
// Usage: node scripts/capture-screenshots.mjs [--fixtures=./fixtures] [--out=./docs/screenshots]
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import net from 'net';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
const argMap = Object.fromEntries(process.argv.slice(2).map(a => {
|
|
17
|
+
const [k, v] = a.replace(/^--/, '').split('=');
|
|
18
|
+
return [k, v ?? true];
|
|
19
|
+
}));
|
|
20
|
+
const FIXTURES = path.resolve(argMap.fixtures || path.join(ROOT, 'fixtures'));
|
|
21
|
+
const OUT = path.resolve(argMap.out || path.join(ROOT, 'docs/screenshots'));
|
|
22
|
+
|
|
23
|
+
function pickChrome() {
|
|
24
|
+
if (process.env.CHROME && fs.existsSync(process.env.CHROME)) return process.env.CHROME;
|
|
25
|
+
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
26
|
+
const candidates = [
|
|
27
|
+
// Linux
|
|
28
|
+
'/usr/bin/chromium',
|
|
29
|
+
'/usr/bin/chromium-browser',
|
|
30
|
+
'/usr/bin/google-chrome',
|
|
31
|
+
'/usr/bin/google-chrome-stable',
|
|
32
|
+
'/snap/bin/chromium',
|
|
33
|
+
// macOS
|
|
34
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
35
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
36
|
+
// Windows - program files variants
|
|
37
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
38
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
39
|
+
path.join(home, 'AppData/Local/Google/Chrome/Application/chrome.exe'),
|
|
40
|
+
path.join(home, 'AppData/Local/Chromium/Application/chrome.exe'),
|
|
41
|
+
// puppeteer-core bundled chromium (if installed via npm)
|
|
42
|
+
...(() => { try { return [require('puppeteer-core').executablePath()]; } catch { return []; } })(),
|
|
43
|
+
];
|
|
44
|
+
for (const c of candidates) if (c && fs.existsSync(c)) return c;
|
|
45
|
+
throw new Error('No chromium binary found. Set CHROME env var, install Google Chrome, or apt install chromium.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function findFreePort() {
|
|
49
|
+
return await new Promise((resolve, reject) => {
|
|
50
|
+
const s = net.createServer();
|
|
51
|
+
s.unref();
|
|
52
|
+
s.on('error', reject);
|
|
53
|
+
s.listen(0, () => {
|
|
54
|
+
const p = s.address().port;
|
|
55
|
+
s.close(() => resolve(p));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function waitForHealthy(url, timeoutMs = 20000) {
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
while (Date.now() - start < timeoutMs) {
|
|
63
|
+
try {
|
|
64
|
+
const r = await fetch(url);
|
|
65
|
+
if (r.ok) return true;
|
|
66
|
+
} catch {}
|
|
67
|
+
await new Promise(r => setTimeout(r, 250));
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Server did not become healthy at ${url} within ${timeoutMs}ms`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
74
|
+
if (!fs.existsSync(FIXTURES)) {
|
|
75
|
+
console.warn(`[capture] fixture dir does not exist: ${FIXTURES} - using empty data dir`);
|
|
76
|
+
fs.mkdirSync(FIXTURES, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const port = await findFreePort();
|
|
80
|
+
const BASE = `http://localhost:${port}/gm`;
|
|
81
|
+
|
|
82
|
+
console.log(`[capture] booting server on :${port} with data=${FIXTURES}`);
|
|
83
|
+
const serverEnv = {
|
|
84
|
+
...process.env,
|
|
85
|
+
PASSWORD: '',
|
|
86
|
+
PORT: String(port),
|
|
87
|
+
HOT_RELOAD: 'false',
|
|
88
|
+
STARTUP_CWD: FIXTURES,
|
|
89
|
+
PORTABLE_DATA_DIR: FIXTURES,
|
|
90
|
+
BASE_URL: '/gm',
|
|
91
|
+
AGENTGUI_SKIP_AUTO_IMPORT: '1', // do not merge user's ~/.claude/projects into the fixture DB
|
|
92
|
+
};
|
|
93
|
+
const server = spawn(process.execPath, [path.join(ROOT, 'server.js')], {
|
|
94
|
+
env: serverEnv,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
cwd: ROOT,
|
|
97
|
+
});
|
|
98
|
+
const srvLog = [];
|
|
99
|
+
server.stdout.on('data', d => { srvLog.push(d); process.stdout.write(d); });
|
|
100
|
+
server.stderr.on('data', d => { srvLog.push(d); process.stderr.write(d); });
|
|
101
|
+
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
try { server.kill('SIGTERM'); } catch {}
|
|
104
|
+
setTimeout(() => { try { server.kill('SIGKILL'); } catch {} }, 1500);
|
|
105
|
+
};
|
|
106
|
+
process.on('exit', cleanup);
|
|
107
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
108
|
+
|
|
109
|
+
let serverExitCode = null;
|
|
110
|
+
server.on('exit', (code) => { serverExitCode = code ?? 0; });
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await waitForHealthy(`${BASE}/api/health`);
|
|
114
|
+
if (serverExitCode !== null) throw new Error(`Server exited with code ${serverExitCode} before becoming healthy`);
|
|
115
|
+
console.log('[capture] server healthy - launching browser');
|
|
116
|
+
|
|
117
|
+
const { default: puppeteer } = await import('puppeteer-core');
|
|
118
|
+
const browser = await puppeteer.launch({
|
|
119
|
+
executablePath: pickChrome(),
|
|
120
|
+
headless: 'new',
|
|
121
|
+
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--hide-scrollbars'],
|
|
122
|
+
defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 1 },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const shots = [
|
|
126
|
+
{ name: 'home-light', theme: 'light', url: `${BASE}/` },
|
|
127
|
+
{ name: 'home-dark', theme: 'dark', url: `${BASE}/` },
|
|
128
|
+
{ name: 'conversation-light', theme: 'light', url: `${BASE}/`, firstConv: true },
|
|
129
|
+
{ name: 'conversation-dark', theme: 'dark', url: `${BASE}/`, firstConv: true },
|
|
130
|
+
{ name: 'tools-manager', theme: 'light', url: `${BASE}/`, openTools: true },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
for (const s of shots) {
|
|
134
|
+
const page = await browser.newPage();
|
|
135
|
+
page.setDefaultNavigationTimeout(30000);
|
|
136
|
+
await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]);
|
|
137
|
+
await page.evaluateOnNewDocument((theme) => {
|
|
138
|
+
try { localStorage.setItem('theme', theme); } catch {}
|
|
139
|
+
if (theme === 'dark') {
|
|
140
|
+
document.documentElement.classList.add('dark');
|
|
141
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
142
|
+
}
|
|
143
|
+
// Determinism: disable transitions/animations so repeated shots match byte-for-byte
|
|
144
|
+
const style = document.createElement('style');
|
|
145
|
+
style.textContent = `*,*::before,*::after{transition:none!important;animation:none!important;caret-color:transparent!important}`;
|
|
146
|
+
if (document.head) document.head.appendChild(style);
|
|
147
|
+
else document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
|
|
148
|
+
}, s.theme);
|
|
149
|
+
|
|
150
|
+
console.log(`[capture] -> ${s.name} (${s.theme})`);
|
|
151
|
+
await page.goto(s.url, { waitUntil: 'domcontentloaded' });
|
|
152
|
+
// Determinism: wait until the sidebar has transitioned out of "Loading..."
|
|
153
|
+
// and rendered at least one conversation row.
|
|
154
|
+
await page.waitForFunction(() => {
|
|
155
|
+
const ul = document.querySelector('.sidebar-list');
|
|
156
|
+
if (!ul) return false;
|
|
157
|
+
const rows = ul.querySelectorAll('li:not(.sidebar-empty)');
|
|
158
|
+
return rows.length > 0;
|
|
159
|
+
}, { timeout: 8000 }).catch(() => {});
|
|
160
|
+
await new Promise(r => setTimeout(r, 600));
|
|
161
|
+
|
|
162
|
+
if (s.firstConv) {
|
|
163
|
+
const clicked = await page.evaluate(() => {
|
|
164
|
+
const row = document.querySelector('.sidebar-list li:not(.sidebar-empty), .conversation-item:not(.sidebar-empty)');
|
|
165
|
+
if (row) { row.click(); return true; }
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
if (clicked) {
|
|
169
|
+
await page.waitForFunction(() => {
|
|
170
|
+
const out = document.querySelector('#output');
|
|
171
|
+
return out && out.children.length > 0;
|
|
172
|
+
}, { timeout: 5000 }).catch(() => {});
|
|
173
|
+
await new Promise(r => setTimeout(r, 600));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (s.openTools) {
|
|
177
|
+
const clicked = await page.evaluate(() => {
|
|
178
|
+
const btn = document.getElementById('toolsManagerBtn');
|
|
179
|
+
if (btn) { btn.click(); return true; }
|
|
180
|
+
return false;
|
|
181
|
+
});
|
|
182
|
+
if (clicked) await new Promise(r => setTimeout(r, 400));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const outPath = path.join(OUT, `${s.name}.png`);
|
|
186
|
+
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
|
187
|
+
const sz = fs.statSync(outPath).size;
|
|
188
|
+
console.log(` wrote ${outPath} (${(sz/1024).toFixed(1)}KB)`);
|
|
189
|
+
await page.close();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await browser.close();
|
|
193
|
+
} finally {
|
|
194
|
+
cleanup();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Final summary
|
|
198
|
+
const produced = fs.readdirSync(OUT).filter(f => f.endsWith('.png')).sort();
|
|
199
|
+
console.log(`\n[capture] done. ${produced.length} png(s) in ${OUT}`);
|
|
200
|
+
for (const f of produced) {
|
|
201
|
+
const p = path.join(OUT, f);
|
|
202
|
+
console.log(` ${f} ${(fs.statSync(p).size/1024).toFixed(1)}KB`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main().catch(e => { console.error(e); process.exit(1); });
|