feat(studio): drop Modules tab, fold its content into Store detail (v0.35.0)

Modules-as-a-tab was redundant with the Store after the
"Installed" filter and the federation work landed. Two pieces
of unique content kept it alive: the per-module declared
permissions list and the on-disk module directory. Both now
live inside the Store detail sheet.

Changes:

- Sidebar nav loses the Modules entry. `_pages` no longer
  carries a `'modules'` slot. The unused `import
  'pages/modules.dart'` is dropped from main.dart. Smoke test
  updated to skip the Modules-text expectation.

- Store detail sheet (`_StoreDetailSheet`) gains two new
  sections, rendered only when the entry is installed and the
  hub returned `ModuleDetail` for it:

    Declared permissions  →  same icon-prefixed list the
                              old Modules sheet shipped
                              (`net:`, `fs.read:`, `fs.write:`,
                              `env:`, `hub:`).
    Module directory      →  selectable mono path so the
                              operator can paste it into a
                              shell.

  An async `moduleInfo` fetch fires from `initState` only when
  `widget.item.installed` is true, so the regular
  not-installed detail-sheet path takes no extra round-trip.
  Failures stay silent — the sections just hide.

- The `FaiModuleSheet` widget stays intact. Cmd+K still uses
  it as a quick-info modal for installed modules; the
  longer-form Store detail sheet covers the same data plus
  the description, screenshots, and docs that operators
  reach for in the Store.

Three new ARB keys: `storeSectionPermissions`,
`storeSectionDirectory`, `storeSectionPermissionsNone`.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-08 14:57:51 +02:00
parent 3b5fb027c3
commit a7e2eb101a
9 changed files with 149 additions and 10 deletions

View file

@ -651,6 +651,21 @@ String _categoryDisplayName(BuildContext ctx, String wire) {
/// Same idea for the `published` / `alpha` / `planned` status
/// values. Operators read "stable / experimental / coming
/// soon"; the wire keeps the strict enum it has always had.
/// Map a permission string (`net:api.example.com`,
/// `fs.read:/etc/fai`, `env:OPENAI_API_KEY`, ) to a glyph the
/// operator can read at a glance. Mirrors the table that used
/// to live in `fai_module_sheet.dart` so the Store detail-sheet
/// can render permissions in the same shape after the Modules
/// page was dropped.
IconData _iconForPermission(String permission) {
if (permission.startsWith('net:')) return Icons.public;
if (permission.startsWith('fs.read:')) return Icons.folder_open;
if (permission.startsWith('fs.write:')) return Icons.edit_note;
if (permission.startsWith('env:')) return Icons.terminal;
if (permission.startsWith('hub:')) return Icons.shield_outlined;
return Icons.lock_outline;
}
String _statusDisplayName(BuildContext ctx, String wire) {
final l = AppLocalizations.of(ctx)!;
switch (wire) {
@ -1796,6 +1811,31 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
String? _toast;
Future<({String errorKind, String text, String sourceUrl})>? _docsFuture;
bool _docsLoaded = false;
/// Live module-info for installed entries: declared
/// permissions and on-disk directory. Pulled lazily so
/// not-installed entries don't run an unnecessary RPC. Stays
/// null when the fetch fails the corresponding sections
/// just hide rather than show a spinner inside a hover sheet.
ModuleDetail? _moduleDetail;
@override
void initState() {
super.initState();
if (widget.item.installed) {
_loadModuleDetail();
}
}
Future<void> _loadModuleDetail() async {
try {
final detail =
await HubService.instance.moduleInfo(widget.item.name);
if (!mounted) return;
setState(() => _moduleDetail = detail);
} catch (_) {
// Silent sections stay hidden, the sheet still works.
}
}
Future<void> _install() async {
setState(() {
@ -2058,6 +2098,64 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
),
const SizedBox(height: FaiSpace.lg),
],
// Live-from-hub sections for installed
// modules. Folded in here in v0.35.0 when the
// standalone Modules page got dropped its
// unique value (declared permissions, on-disk
// directory) belongs in the same surface
// operators already use for everything else
// about a module.
if (item.installed && _moduleDetail != null) ...[
_SectionHeader(l.storeSectionPermissions),
const SizedBox(height: FaiSpace.sm),
if (_moduleDetail!.permissions.isEmpty)
Text(
l.storeSectionPermissionsNone,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final p in _moduleDetail!.permissions)
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(
_iconForPermission(p),
size: 14,
color:
theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: FaiSpace.sm),
SelectableText(
p,
style: FaiTheme.mono(
size: 12,
color: theme.colorScheme.onSurface,
),
),
],
),
),
],
),
const SizedBox(height: FaiSpace.lg),
_SectionHeader(l.storeSectionDirectory),
const SizedBox(height: FaiSpace.sm),
SelectableText(
_moduleDetail!.directory,
style: FaiTheme.mono(
size: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: FaiSpace.lg),
],
if (item.screenshotUrls.isNotEmpty) ...[
_SectionHeader(l.storeSectionScreenshots),
const SizedBox(height: FaiSpace.sm),