agentgui 1.0.1106 → 1.0.1108

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 CHANGED
Binary file
package/.gm/prd.yml CHANGED
@@ -3212,3 +3212,67 @@
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: kit-tab-switch-transition
3216
+ subject: Add a subtle opacity/translateY entrance transition (gated by prefers-reduced-motion) for full-page tab-switch/page-body swaps in the kit's app-shell.css, matching the existing craft level of ds-alert-in/ds-card-in/agentchat-jump.show treatments - currently tab switches in agentgui's navTo swap whole page bodies with no motion at all
3217
+ status: completed
3218
+ witness: 'Added per-tab distinct-named keyframe animations (ds-main-in-chat/-history/-files/-live/-settings) bound to each agentgui-main-<tab> modifier class in app-surfaces.css, gated behind prefers-reduced-motion:no-preference. A shared single animation-name on the constant .agentgui-main base class would never replay (CSS animations only restart when the animation-name value itself changes), so each tab needed its own distinct name. Live browser witness on localhost:3000/gm/: clicking History tab -> agentgui-main gains class agentgui-main-history + computed animationName=ds-main-in-history; clicking Files tab -> class agentgui-main-files + animationName=ds-main-in-files. Confirms the entrance animation genuinely re-triggers on each real tab switch, matching the existing ds-alert-in/ds-card-in motion craft level.'
3219
+ - id: kit-avatar-consolidate-fallback
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).'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1106",
3
+ "version": "1.0.1108",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -44,6 +44,9 @@ const state = {
44
44
  eventsLimit: 300, // how many of the most-recent events to render; grows via "load older"
45
45
  files: { path: '', segments: [], entries: [], roots: [], loading: false, error: null, preview: null, sort: 'name', sortDir: 'asc', filter: '' },
46
46
  git: { loading: false, error: null, diff: '', commits: [], worktrees: [], files: [], file: '', worktreeBusy: false },
47
+ // One-time welcome banner naming what each tab is for. Shown until
48
+ // dismissed once, ever - a returning user has already learned the tabs.
49
+ showOnboarding: lsGet('agentgui.onboarded') !== '1',
47
50
  };
48
51
 
49
52
  // Two-step arm controls auto-reset after this delay so an accidental first click
@@ -170,6 +173,12 @@ function lsGet(k) { try { return localStorage.getItem(k); } catch { return null;
170
173
  function lsSet(k, v) { try { localStorage.setItem(k, v); } catch {} }
171
174
  function lsRemove(k) { try { localStorage.removeItem(k); } catch {} }
172
175
 
176
+ function dismissOnboarding() {
177
+ state.showOnboarding = false;
178
+ lsSet('agentgui.onboarded', '1');
179
+ render();
180
+ }
181
+
173
182
  // A single visually-hidden aria-live region for transient announcements (tab
174
183
  // changes, etc.) so screen-reader users hear context that's otherwise conveyed
175
184
  // only by focus movement or color.
@@ -669,7 +678,18 @@ function view() {
669
678
  h('div', { key: 'sc-body', class: 'ds-alert-body' }, ShortcutList({ shortcuts: SHORTCUTS })),
670
679
  ] }))
671
680
  : null;
672
- const main = h('div', { id: 'agentgui-main', role: 'region', 'aria-label': 'main content', 'data-chat-scroll': '', class: 'agentgui-main agentgui-main-' + state.tab }, [shortcutsHint, ...mainContent()].filter(Boolean));
681
+ // One-time welcome naming what each tab is for - a brand-new user's only
682
+ // other guidance is installHint (zero-agents case) or the calm chat empty
683
+ // state (agents-but-no-conversation case); neither explains History/Files/
684
+ // Live exist at all. Dismissed once, ever, via localStorage.
685
+ const onboardingBanner = state.showOnboarding
686
+ ? Alert({
687
+ key: 'onboarding', kind: 'info', title: 'Welcome to AgentGUI',
688
+ onDismiss: dismissOnboarding,
689
+ children: 'Chat with an agent here. History holds every past conversation, Files browses and edits project directories, Live shows every running session at once, and Settings covers connection, appearance, and keyboard shortcuts.',
690
+ })
691
+ : null;
692
+ const main = h('div', { id: 'agentgui-main', role: 'region', 'aria-label': 'main content', 'data-chat-scroll': '', class: 'agentgui-main agentgui-main-' + state.tab }, [shortcutsHint, onboardingBanner, ...mainContent()].filter(Boolean));
673
693
 
674
694
  // Claude-Desktop three-column shell: a persistent left rail (workspace nav), an
675
695
  // optional sessions column (chat + history share the conversation list), the
@@ -1215,7 +1235,9 @@ async function runBulkDelete() {
1215
1235
  state.files.dialog = null;
1216
1236
  restoreFileDialogFocus(d._trigger);
1217
1237
  marked.clear(); state.files._lastMarkIdx = null;
1218
- announce('deleted ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
1238
+ const successMsg = 'deleted ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries');
1239
+ announce(successMsg);
1240
+ toast({ message: successMsg, kind: 'success' });
1219
1241
  // Patch the visible list immediately instead of waiting on a second full
1220
1242
  // directory round-trip - the server already confirmed every deletion.
1221
1243
  const gone = new Set(targets.map((e) => e.path || e.name));
@@ -1265,7 +1287,9 @@ async function runBulkMove(destDir) {
1265
1287
  state.files.dialog = null;
1266
1288
  restoreFileDialogFocus(d._trigger);
1267
1289
  marked.clear(); state.files._lastMarkIdx = null;
1268
- announce('moved ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
1290
+ const successMsg = 'moved ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries');
1291
+ announce(successMsg);
1292
+ toast({ message: successMsg, kind: 'success' });
1269
1293
  // Moved entries leave the current dir - patch them out immediately rather
1270
1294
  // than waiting on a second full directory round-trip.
1271
1295
  const gone = new Set(targets.map((e) => e.path || e.name));
@@ -1301,8 +1325,11 @@ async function runFileMutation(fn, doneMsg, patch) {
1301
1325
  restoreFileDialogFocus(d._trigger);
1302
1326
  announce(doneMsg);
1303
1327
  // A soft-delete's trashId (if this mutation was a delete) rides the
1304
- // return value straight to the undo-toast caller.
1328
+ // return value straight to the undo-toast caller - that banner is its own
1329
+ // dedicated real-undo-action UI, not a generic dismiss-only confirmation,
1330
+ // so it stays separate from toast() rather than being replaced by it.
1305
1331
  if (result && result.trashId) offerUndoDelete(result.trashId, doneMsg);
1332
+ else toast({ message: doneMsg, kind: 'success' });
1306
1333
  if (patch) {
1307
1334
  // Patch the visible list immediately, matching the bulk-delete/move
1308
1335
  // pattern, instead of stalling the dialog on a second full round-trip.
@@ -3790,9 +3817,9 @@ function settingsMain() {
3790
3817
  render();
3791
3818
  },
3792
3819
  }),
3793
- state.backendStatus === 'connecting' ? h('p', { key: 'bst-connecting', class: 't-meta', role: 'status' }, 'connecting…') : null,
3794
- state.backendStatus === 'ok' ? h('p', { key: 'bst-ok', class: 't-meta', role: 'status' }, 'connected') : null,
3795
- state.backendStatus === 'failed' ? h('p', { key: 'bst-failed', class: 't-meta field-error', role: 'alert' }, 'connection failed - check the URL') : null,
3820
+ state.backendStatus === 'connecting' ? h('span', { key: 'bst-connecting', class: 'ds-status-chip', role: 'status' }, 'connecting…') : null,
3821
+ state.backendStatus === 'ok' ? h('span', { key: 'bst-ok', class: 'ds-status-chip ds-status-chip-ok', role: 'status' }, 'connected') : null,
3822
+ state.backendStatus === 'failed' ? h('span', { key: 'bst-failed', class: 'ds-status-chip ds-status-chip-error', role: 'alert' }, 'connection failed - check the URL') : null,
3796
3823
  (state.confirmingBackend !== undefined && state.confirmingBackend === state.backendDraft && isValid && state.backendDraft !== state.backend)
3797
3824
  ? h('p', { key: 'bcw', class: 't-meta field-error', role: 'alert' }, 'changing backend discards this browser\'s chat transcript - press save again to confirm') : null,
3798
3825
  healthSummary(),
@@ -5451,6 +5451,7 @@
5451
5451
  .ds-247420 .ds-avatar-sm { width: 24px; height: 24px; font-size: 10px; }
5452
5452
  .ds-247420 .ds-avatar-md { width: 36px; height: 36px; font-size: 12px; }
5453
5453
  .ds-247420 .ds-avatar-lg { width: 48px; height: 48px; font-size: 15px; }
5454
+ .ds-247420 .ds-avatar-square { border-radius: var(--r-2, 6px); }
5454
5455
 
5455
5456
  /* SpreadsheetPreview — tabbed inline spreadsheet/CSV viewer (docstudio
5456
5457
  xls-preview.js port). Tabs reuse the same neutral-at-rest pattern as
@@ -10477,6 +10478,32 @@
10477
10478
  .ds-247420 .agentgui-main-chat { display: flex; flex-direction: column; flex: 1; }
10478
10479
  .ds-247420 .agentgui-main .ds-section { margin: 0 0 var(--space-4, 16px); }
10479
10480
 
10481
+ /* Tab-switch entrance: navTo() sets a per-tab modifier class
10482
+ (agentgui-main-<tab>) on the SAME #agentgui-main DOM node every switch
10483
+ (webjsx patches props on the existing element rather than replacing it).
10484
+ A single shared animation-name bound to the constant .agentgui-main base
10485
+ class would only ever fire once (on first mount) since a CSS animation
10486
+ restarts only when the animation-name value itself changes or the
10487
+ element is detached/reattached - the base class never changes. Each tab
10488
+ gets its OWN animation-name (functionally identical keyframes, distinct
10489
+ names) so switching from e.g. agentgui-main-chat to agentgui-main-history
10490
+ genuinely changes which animation-name applies, which is what makes the
10491
+ browser replay the entrance on every switch. Matches the existing motion
10492
+ craft level of ds-alert-in/ds-card-in/agentchat-jump.show instead of every
10493
+ full-page tab swap happening with zero transition. */
10494
+ @media (prefers-reduced-motion: no-preference) {
10495
+ .ds-247420 .agentgui-main-chat { animation: ds-main-in-chat var(--dur-base) var(--ease); }
10496
+ .ds-247420 .agentgui-main-history { animation: ds-main-in-history var(--dur-base) var(--ease); }
10497
+ .ds-247420 .agentgui-main-files { animation: ds-main-in-files var(--dur-base) var(--ease); }
10498
+ .ds-247420 .agentgui-main-live { animation: ds-main-in-live var(--dur-base) var(--ease); }
10499
+ .ds-247420 .agentgui-main-settings { animation: ds-main-in-settings var(--dur-base) var(--ease); }
10500
+ }
10501
+ @keyframes ds-main-in-chat { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
10502
+ @keyframes ds-main-in-history { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
10503
+ @keyframes ds-main-in-files { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
10504
+ @keyframes ds-main-in-live { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
10505
+ @keyframes ds-main-in-settings { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
10506
+
10480
10507
  /* Readable backend health summary (replaces raw JSON dump). */
10481
10508
  .ds-247420 .health-summary { display: flex; flex-wrap: wrap; gap: .4em; margin: .6em 0; }
10482
10509
  .ds-247420 .health-chip {
@@ -10492,6 +10519,30 @@
10492
10519
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
10493
10520
  }
10494
10521
 
10522
+ /* Standalone connection-status chip — same visual vocabulary as
10523
+ .health-chip so a form's own connecting/connected/failed feedback reads
10524
+ consistently with the healthSummary row beneath it, instead of one being
10525
+ a colored chip and the other bare <p class="t-meta"> text. */
10526
+ .ds-247420 .ds-status-chip {
10527
+ display: inline-flex; align-items: center; gap: .35em;
10528
+ font-family: var(--ff-mono, ui-monospace, monospace);
10529
+ font-size: .8rem; padding: .15em .55em; border-radius: var(--r-1, 6px);
10530
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
10531
+ color: var(--fg-2);
10532
+ border: var(--bw-hair, 1px) solid color-mix(in srgb, var(--fg) 12%, transparent);
10533
+ margin: .4em 0;
10534
+ }
10535
+ .ds-247420 .ds-status-chip-ok {
10536
+ color: var(--accent-ink);
10537
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
10538
+ border-color: color-mix(in srgb, var(--accent) 40%, transparent);
10539
+ }
10540
+ .ds-247420 .ds-status-chip-error {
10541
+ color: var(--danger);
10542
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
10543
+ border-color: color-mix(in srgb, var(--danger) 35%, transparent);
10544
+ }
10545
+
10495
10546
  /* Compact working-directory bar. */
10496
10547
  .ds-247420 .cwd-bar {
10497
10548
  display: flex; align-items: center; gap: .5em; flex-wrap: wrap;