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.
@@ -3,7 +3,7 @@ import * as B from './backend.js';
3
3
 
4
4
  installStyles().catch(() => {});
5
5
 
6
- const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, TextField, Select, Btn, Icon, IconButton, EventList, Spinner, Alert, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker, BreadcrumbPath, EmptyState, FileViewer, FilePreviewPane, FilePreviewCode, FilePreviewText, FilePreviewMedia, ThemeToggle, ContextPane, PromptDialog, ConfirmDialog, DropZone, UploadProgress, FilterPills, SessionMeta, BulkBar, Checkbox, ShortcutList, FocusTrap, AgentListSkeleton, flashComposerNote, toast, withBusy, GitStatusPanel, GitDiffView, WorktreeSwitcher } = C;
6
+ const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, TextField, Select, Btn, Icon, IconButton, EventList, Spinner, Alert, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker, BreadcrumbPath, EmptyState, FileViewer, FilePreviewPane, FilePreviewCode, FilePreviewText, FilePreviewMedia, ThemeToggle, ContextPane, PromptDialog, ConfirmDialog, DropZone, UploadProgress, FilterPills, SessionMeta, BulkBar, Checkbox, ShortcutList, FocusTrap, AgentListSkeleton, flashComposerNote, toast, withBusy, GitStatusPanel, GitDiffView, WorktreeSwitcher, ModelsConfig, PluginsConfig } = C;
7
7
 
8
8
  // One duration/bytes vocabulary across every surface: prefer the kit's shared
9
9
  // formatters (exported alongside the components), fall back to the local
@@ -44,9 +44,13 @@ 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
+ // Models tab: composed agent/provider availability (models.availability WS
48
+ // handler), fed into the design SDK's ModelsConfig component.
49
+ models: { data: null, loading: false, error: null, selectedProviderId: null },
50
+ // Plugins tab: agentgui has no freddie-style plugin host - the closest real
51
+ // extensibility surface is the discovered agent-CLI registry (agents.list),
52
+ // adapted to PluginsConfig's {name,surfaces,requires,enabled,status} shape.
53
+ plugins: { selected: null },
50
54
  };
51
55
 
52
56
  // Two-step arm controls auto-reset after this delay so an accidental first click
@@ -173,12 +177,6 @@ function lsGet(k) { try { return localStorage.getItem(k); } catch { return null;
173
177
  function lsSet(k, v) { try { localStorage.setItem(k, v); } catch {} }
174
178
  function lsRemove(k) { try { localStorage.removeItem(k); } catch {} }
175
179
 
176
- function dismissOnboarding() {
177
- state.showOnboarding = false;
178
- lsSet('agentgui.onboarded', '1');
179
- render();
180
- }
181
-
182
180
  // A single visually-hidden aria-live region for transient announcements (tab
183
181
  // changes, etc.) so screen-reader users hear context that's otherwise conveyed
184
182
  // only by focus movement or color.
@@ -326,6 +324,24 @@ function navTo(tab, { writeHash: doWriteHash = true, push = true } = {}) {
326
324
  // filesMain (which runs during render) - a render-time fetch is fragile under
327
325
  // a double-render and re-enters render() while building the tree.
328
326
  if (tab === 'files' && !state.files.path && !state.files.loading && !state.files.error) loadDir('');
327
+ // Chat's @-mention autocomplete (mentionFiles) reuses state.files.entries -
328
+ // seed it from the root listing on first chat visit so mentions work
329
+ // without requiring a prior Files-tab visit. Uses the same confined
330
+ // B.listDir the Files tab itself calls; failure is silent (mentionFiles
331
+ // just stays empty, same as never having visited Files).
332
+ if (tab === 'chat' && !state.files.entries.length && !state.files.loading && !state.files.error) {
333
+ B.listDir(state.backend, '').then((j) => {
334
+ if (!state.files.entries.length) state.files.entries = j.entries || [];
335
+ if (!state.files.roots.length) state.files.roots = j.roots || [];
336
+ render();
337
+ }).catch(() => {});
338
+ }
339
+ // Models tab: composed agent/provider availability, fetched on first visit
340
+ // (and via the ModelsConfig 'refresh' action thereafter).
341
+ if (tab === 'models' && !state.models.data && !state.models.loading && !state.models.error) loadModelsAvailability();
342
+ // Plugins tab reuses state.agents (agents.list) - same lazy load as chat's
343
+ // agent picker, so visiting Plugins directly (deep-link/reload) still works.
344
+ if (tab === 'plugins' && !state.agents.length && !state.agentsLoading && !state.agentsError) loadAgents();
329
345
  // popstate calls navTo with writeHash:false so it never replaceState-clobbers
330
346
  // the entry it popped; user-initiated navigation pushes so Back walks tabs.
331
347
  if (doWriteHash) writeHash({ push });
@@ -656,6 +672,10 @@ function view() {
656
672
  } else if (state.tab === 'settings') {
657
673
  // Same word as the rail item - location chrome must not fork vocabulary.
658
674
  crumbLeaf = 'settings';
675
+ } else if (state.tab === 'models') {
676
+ crumbLeaf = 'models';
677
+ } else if (state.tab === 'plugins') {
678
+ crumbLeaf = 'plugins';
659
679
  }
660
680
  const crumb = Crumb({ trail: crumbTrail, leaf: crumbLeaf, right: [dot] });
661
681
 
@@ -678,18 +698,7 @@ function view() {
678
698
  h('div', { key: 'sc-body', class: 'ds-alert-body' }, ShortcutList({ shortcuts: SHORTCUTS })),
679
699
  ] }))
680
700
  : null;
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));
701
+ 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));
693
702
 
694
703
  // Claude-Desktop three-column shell: a persistent left rail (workspace nav), an
695
704
  // optional sessions column (chat + history share the conversation list), the
@@ -749,6 +758,8 @@ function workspaceRail() {
749
758
  { key: 'files', label: 'Files', icon: 'folder', active: state.tab === 'files', onClick: () => navTo('files') },
750
759
  { key: 'live', label: 'Live', icon: 'activity', active: state.tab === 'live', count: liveCount || null, rail: liveHasError ? 'flame' : undefined, onClick: () => navTo('live') },
751
760
  { key: 'git', label: 'Git', icon: 'branch', active: state.tab === 'git', onClick: () => navTo('git') },
761
+ { key: 'models', label: 'Models', icon: 'circle-dot', active: state.tab === 'models', onClick: () => navTo('models') },
762
+ { key: 'plugins', label: 'Plugins', icon: 'link', active: state.tab === 'plugins', onClick: () => navTo('plugins') },
752
763
  { key: 'settings', label: 'Settings', icon: 'settings', active: state.tab === 'settings', onClick: () => navTo('settings') },
753
764
  ];
754
765
  return WorkspaceRail({
@@ -953,6 +964,8 @@ function mainContent() {
953
964
  if (state.tab === 'files') return filesMain();
954
965
  if (state.tab === 'live') return liveMain();
955
966
  if (state.tab === 'git') return gitMain();
967
+ if (state.tab === 'models') return modelsMain();
968
+ if (state.tab === 'plugins') return pluginsMain();
956
969
  return settingsMain();
957
970
  }
958
971
 
@@ -1055,6 +1068,61 @@ function gitMain() {
1055
1068
  ];
1056
1069
  }
1057
1070
 
1071
+ // Models tab: agentgui's real model surface composed by the models.availability
1072
+ // WS handler (agent-CLI registry availability + provider key presence), fed
1073
+ // into the design SDK's ModelsConfig component. Not freddie's probed per-mode
1074
+ // matrix - see lib/ws-handlers-util.js models.availability for the mapping.
1075
+ function modelsMain() {
1076
+ const m = state.models;
1077
+ return [
1078
+ PageHeader({ compact: true, dense: true, title: 'Models', lede: 'Discovered agent CLIs, their models, and provider key presence on this server.' }),
1079
+ ModelsConfig({
1080
+ data: m.data,
1081
+ loading: m.loading,
1082
+ error: m.error,
1083
+ selectedProviderId: m.selectedProviderId,
1084
+ onSelectProvider: (id) => { state.models.selectedProviderId = id; render(); },
1085
+ onRefresh: () => loadModelsAvailability(),
1086
+ }),
1087
+ ];
1088
+ }
1089
+
1090
+ // Plugins tab: agentgui has no freddie-style plugin host (~150 discoverable
1091
+ // plugin.js files) - its actual extensibility surface is the discovered
1092
+ // agent-CLI registry (lib/agent-discovery.js + lib/claude-runner-agents.js
1093
+ // AgentRegistry, the same data agents.list already exposes to the chat
1094
+ // picker). Adapted onto PluginsConfig's shape: name=agent id, surfaces=
1095
+ // protocol (cli|acp), requires=[] (agents have no dependency graph), enabled=
1096
+ // available (the CLI was actually found on this server), status=install hint
1097
+ // when missing. There is no enable/disable action (agentgui does not gate
1098
+ // which agent CLIs are usable) so onToggle re-checks availability instead of
1099
+ // pretending to flip a setting that doesn't exist.
1100
+ function pluginsMain() {
1101
+ const list = (state.agents || []).map((a) => ({
1102
+ name: a.id,
1103
+ version: undefined,
1104
+ surfaces: a.protocol || 'cli',
1105
+ requires: [],
1106
+ source: a.npxPackage ? ('npx ' + a.npxPackage) : undefined,
1107
+ enabled: a.available !== false,
1108
+ status: a.available !== false ? 'loaded' : (a.npxInstallable ? 'installable via npx' : 'not found'),
1109
+ }));
1110
+ return [
1111
+ PageHeader({ compact: true, dense: true, title: 'Plugins', lede: 'Discovered agent CLIs — agentgui\'s real extensibility surface (no freddie-style plugin host).' }),
1112
+ PluginsConfig({
1113
+ plugins: list,
1114
+ selected: state.plugins.selected,
1115
+ loading: !!state.agentsLoading,
1116
+ error: state.agentsError,
1117
+ onSelect: (name) => { state.plugins.selected = name; render(); },
1118
+ // No real enable/disable exists - re-run discovery so a just-installed
1119
+ // CLI's availability reflects immediately instead of a fake toggle.
1120
+ onToggle: () => { withBusy(null, () => loadAgents(), 'checking…'); },
1121
+ onReload: () => loadAgents(),
1122
+ }),
1123
+ ];
1124
+ }
1125
+
1058
1126
  // --- files (folder browser) ---
1059
1127
  async function loadDir(dirPath, { fromHash = false } = {}) {
1060
1128
  state.files = state.files || {};
@@ -1235,9 +1303,7 @@ async function runBulkDelete() {
1235
1303
  state.files.dialog = null;
1236
1304
  restoreFileDialogFocus(d._trigger);
1237
1305
  marked.clear(); state.files._lastMarkIdx = null;
1238
- const successMsg = 'deleted ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries');
1239
- announce(successMsg);
1240
- toast({ message: successMsg, kind: 'success' });
1306
+ announce('deleted ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
1241
1307
  // Patch the visible list immediately instead of waiting on a second full
1242
1308
  // directory round-trip - the server already confirmed every deletion.
1243
1309
  const gone = new Set(targets.map((e) => e.path || e.name));
@@ -1287,9 +1353,7 @@ async function runBulkMove(destDir) {
1287
1353
  state.files.dialog = null;
1288
1354
  restoreFileDialogFocus(d._trigger);
1289
1355
  marked.clear(); state.files._lastMarkIdx = null;
1290
- const successMsg = 'moved ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries');
1291
- announce(successMsg);
1292
- toast({ message: successMsg, kind: 'success' });
1356
+ announce('moved ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
1293
1357
  // Moved entries leave the current dir - patch them out immediately rather
1294
1358
  // than waiting on a second full directory round-trip.
1295
1359
  const gone = new Set(targets.map((e) => e.path || e.name));
@@ -1325,11 +1389,8 @@ async function runFileMutation(fn, doneMsg, patch) {
1325
1389
  restoreFileDialogFocus(d._trigger);
1326
1390
  announce(doneMsg);
1327
1391
  // A soft-delete's trashId (if this mutation was a delete) rides the
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.
1392
+ // return value straight to the undo-toast caller.
1331
1393
  if (result && result.trashId) offerUndoDelete(result.trashId, doneMsg);
1332
- else toast({ message: doneMsg, kind: 'success' });
1333
1394
  if (patch) {
1334
1395
  // Patch the visible list immediately, matching the bulk-delete/move
1335
1396
  // pattern, instead of stalling the dialog on a second full round-trip.
@@ -2483,6 +2544,16 @@ function chatMain() {
2483
2544
  messages: state.chat.messages,
2484
2545
  busy: state.chat.busy,
2485
2546
  draft: state.chat.draft,
2547
+ // Scroll-position overview strip alongside the thread.
2548
+ showMinimap: true,
2549
+ // @-mention file autocomplete: reuse the Files tab's own data source
2550
+ // (state.files.entries, populated by loadDir/listDir) rather than a new
2551
+ // fetch. Scoped to whatever directory Files last listed - if the user
2552
+ // hasn't opened Files yet this is empty and the composer simply shows
2553
+ // no mention suggestions until they do (no fake/synthetic file list).
2554
+ mentionFiles: (state.files.entries || [])
2555
+ .filter((e) => e && e.path)
2556
+ .map((e) => ({ path: e.path, isDir: e.type === 'dir' })),
2486
2557
  // Idle never reads 'resuming…' (nothing is in flight - the continuation
2487
2558
  // fact lives in the composer context line and banner); a remotely-stopped
2488
2559
  // turn reads 'stopped', not a normal finish.
@@ -3817,9 +3888,9 @@ function settingsMain() {
3817
3888
  render();
3818
3889
  },
3819
3890
  }),
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,
3891
+ state.backendStatus === 'connecting' ? h('p', { key: 'bst-connecting', class: 't-meta', role: 'status' }, 'connecting…') : null,
3892
+ state.backendStatus === 'ok' ? h('p', { key: 'bst-ok', class: 't-meta', role: 'status' }, 'connected') : null,
3893
+ state.backendStatus === 'failed' ? h('p', { key: 'bst-failed', class: 't-meta field-error', role: 'alert' }, 'connection failed - check the URL') : null,
3823
3894
  (state.confirmingBackend !== undefined && state.confirmingBackend === state.backendDraft && isValid && state.backendDraft !== state.backend)
3824
3895
  ? 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,
3825
3896
  healthSummary(),
@@ -4266,6 +4337,25 @@ async function loadAgents() {
4266
4337
  }
4267
4338
  }
4268
4339
 
4340
+ // Models tab data load: models.availability WS handler composes agent
4341
+ // registry + provider key presence into ModelsConfig's expected shape. See
4342
+ // lib/ws-handlers-util.js for exactly what each field means in agentgui (no
4343
+ // freddie-style per-mode probe matrix - one real 'cli' mode per model).
4344
+ async function loadModelsAvailability() {
4345
+ state.models.loading = true;
4346
+ state.models.error = null;
4347
+ render();
4348
+ try {
4349
+ state.models.data = await B.getModelsAvailability(state.backend);
4350
+ state.models.loading = false;
4351
+ render();
4352
+ } catch (e) {
4353
+ state.models.loading = false;
4354
+ state.models.error = errText(e) || 'failed to load model availability';
4355
+ render();
4356
+ }
4357
+ }
4358
+
4269
4359
  // Boot-time automatic retries with backoff when the first agents fetch fails
4270
4360
  // (the server may still be warming up).
4271
4361
  async function retryLoadAgents() {
@@ -428,6 +428,25 @@ export async function listAgentModels(base, agentId) {
428
428
  } catch { return []; }
429
429
  }
430
430
 
431
+ // Composed model-availability view for the Models tab (ModelsConfig
432
+ // component): per-agent CLI availability + models, in the shape ModelsConfig
433
+ // expects ({timestamp, providers, sampler, summary}). See lib/ws-handlers-
434
+ // util.js models.availability for what each field really means in agentgui
435
+ // (no probed per-mode matrix like freddie - one real 'cli' mode per model).
436
+ export async function getModelsAvailability(base) {
437
+ return wsCall(base, 'models.availability', {});
438
+ }
439
+
440
+ // Provider API-key configs (masked) for the Models tab's provider auth info -
441
+ // backs the existing Settings 'keys' surface, reused here as-is.
442
+ export async function getAuthConfigs(base) {
443
+ return wsCall(base, 'auth.configs', {});
444
+ }
445
+
446
+ export async function saveAuthConfig(base, providerId, apiKey, defaultModel) {
447
+ return wsCall(base, 'auth.save', { providerId, apiKey, defaultModel });
448
+ }
449
+
431
450
  // ---------- Git / worktrees (WS) ----------
432
451
 
433
452
  export async function gitStatus(base, { cwd } = {}) {