feat(studio): per-field ports in flow editor + store-doc scroll fix
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Picks up: - fai_studio_flow_editor 0.9.0: per-field input + output ports rendered from ModuleInfo, i18n tooltips, four additional canvas patterns ported from jai_client. - fai_client_sdk 0.18.0: regenerated ModuleField bindings for the schema_version 3 / per-field-description proto bump in fai/platform. Wires StudioFlowRunDriver.moduleInfo so the editor can fetch declared field info per step capability. The hub maps schema_version 3 manifest fields (inputs/outputs with description.en/de) through ModuleInfoResponse; this patch wraps them back into the editor's ModuleSpec / ModuleField vocabulary so the canvas paints them as distinct anchors with hover tooltips. ModuleDetail in hub.dart now carries the same inputs + outputs lists (with ModuleFieldInfo type + i18n description map), enabling other Studio surfaces — e.g. the module-detail sheet — to render bilingual field docs in a later pass. Plus: store-doc nested-scroll fix. The module detail panel's docs section sat inside its own 480px viewport inside the outer sheet's SingleChildScrollView, producing the double-scrollbar feel. Setting physics: NeverScrollableScrollPhysics on the Markdown widget and dropping the maxHeight lets the docs scroll as part of the outer sheet — one scrollbar, predictable mouse-wheel behaviour. Bumps fai_studio to 0.56.0. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
34d086c29b
commit
a698be0b57
5 changed files with 466 additions and 336 deletions
|
|
@ -40,6 +40,39 @@ class StudioFlowRunDriver implements editor.FlowRunDriver {
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<editor.ModuleSpec?> moduleInfo(String capability) async {
|
||||||
|
try {
|
||||||
|
final detail = await HubService.instance.moduleInfo(capability);
|
||||||
|
return editor.ModuleSpec(
|
||||||
|
capability: capability,
|
||||||
|
inputs: detail.inputs
|
||||||
|
.map(
|
||||||
|
(f) => editor.ModuleField(
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
description: f.description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
outputs: detail.outputs
|
||||||
|
.map(
|
||||||
|
(f) => editor.ModuleField(
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
description: f.description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Module not installed locally, or transient hub error.
|
||||||
|
// Returning null lets the editor fall back to YAML-
|
||||||
|
// derived ports instead of failing the whole canvas.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<editor.FlowRunEvent> events() {
|
Stream<editor.FlowRunEvent> events() {
|
||||||
return HubService.instance
|
return HubService.instance
|
||||||
|
|
@ -82,10 +115,7 @@ class StudioFlowRunDriver implements editor.FlowRunDriver {
|
||||||
error: e.error ?? '',
|
error: e.error ?? '',
|
||||||
);
|
);
|
||||||
case 'step.awaiting_approval':
|
case 'step.awaiting_approval':
|
||||||
return editor.StepAwaitingApproval(
|
return editor.StepAwaitingApproval(flowName: flowName, stepId: stepId);
|
||||||
flowName: flowName,
|
|
||||||
stepId: stepId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,12 +133,9 @@ class HubService {
|
||||||
return ModuleSummary(
|
return ModuleSummary(
|
||||||
name: e.key,
|
name: e.key,
|
||||||
version: caps.first.moduleVersion,
|
version: caps.first.moduleVersion,
|
||||||
capabilities: caps
|
capabilities: caps.map((c) => '${c.capability}@${c.version}').toList(),
|
||||||
.map((c) => '${c.capability}@${c.version}')
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
}).toList()
|
}).toList()..sort((a, b) => a.name.compareTo(b.name));
|
||||||
..sort((a, b) => a.name.compareTo(b.name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every capability the hub can execute, with the `kind` tag
|
/// Every capability the hub can execute, with the `kind` tag
|
||||||
|
|
@ -151,7 +148,8 @@ class HubService {
|
||||||
Future<List<CapabilityInfo>> allCapabilities() async {
|
Future<List<CapabilityInfo>> allCapabilities() async {
|
||||||
final caps = await _client.listCapabilities();
|
final caps = await _client.listCapabilities();
|
||||||
return caps
|
return caps
|
||||||
.map((c) => CapabilityInfo(
|
.map(
|
||||||
|
(c) => CapabilityInfo(
|
||||||
capability: c.capability,
|
capability: c.capability,
|
||||||
version: c.version,
|
version: c.version,
|
||||||
moduleName: c.moduleName,
|
moduleName: c.moduleName,
|
||||||
|
|
@ -159,7 +157,8 @@ class HubService {
|
||||||
provider: c.provider,
|
provider: c.provider,
|
||||||
sourceKind: c.sourceKind,
|
sourceKind: c.sourceKind,
|
||||||
versionAdvisory: c.versionAdvisory,
|
versionAdvisory: c.versionAdvisory,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,6 +174,24 @@ class HubService {
|
||||||
permissions: r.permissions,
|
permissions: r.permissions,
|
||||||
directory: r.directory,
|
directory: r.directory,
|
||||||
acceptsMime: r.acceptsMime,
|
acceptsMime: r.acceptsMime,
|
||||||
|
inputs: r.inputs
|
||||||
|
.map(
|
||||||
|
(f) => ModuleFieldInfo(
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
description: Map<String, String>.from(f.description),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
outputs: r.outputs
|
||||||
|
.map(
|
||||||
|
(f) => ModuleFieldInfo(
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
description: Map<String, String>.from(f.description),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,8 +317,7 @@ class HubService {
|
||||||
/// Pull (download) an Ollama model via /api/pull.
|
/// Pull (download) an Ollama model via /api/pull.
|
||||||
/// Synchronous; can take minutes for large models. Errors
|
/// Synchronous; can take minutes for large models. Errors
|
||||||
/// come back as `errorKind`.
|
/// come back as `errorKind`.
|
||||||
Future<({String errorKind, String text, int elapsedMs})>
|
Future<({String errorKind, String text, int elapsedMs})> pullSystemAiModel({
|
||||||
pullSystemAiModel({
|
|
||||||
required String endpoint,
|
required String endpoint,
|
||||||
required String model,
|
required String model,
|
||||||
String apiKeyEnv = '',
|
String apiKeyEnv = '',
|
||||||
|
|
@ -311,11 +327,7 @@ class HubService {
|
||||||
model: model,
|
model: model,
|
||||||
apiKeyEnv: apiKeyEnv,
|
apiKeyEnv: apiKeyEnv,
|
||||||
);
|
);
|
||||||
return (
|
return (errorKind: r.errorKind, text: r.text, elapsedMs: r.elapsedMs);
|
||||||
errorKind: r.errorKind,
|
|
||||||
text: r.text,
|
|
||||||
elapsedMs: r.elapsedMs,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detected host hardware. Used by the System-AI editor to
|
/// Detected host hardware. Used by the System-AI editor to
|
||||||
|
|
@ -368,10 +380,7 @@ class HubService {
|
||||||
required String reviewer,
|
required String reviewer,
|
||||||
required String reason,
|
required String reason,
|
||||||
}) async {
|
}) async {
|
||||||
final r = await _client.clearEventLog(
|
final r = await _client.clearEventLog(reviewer: reviewer, reason: reason);
|
||||||
reviewer: reviewer,
|
|
||||||
reason: reason,
|
|
||||||
);
|
|
||||||
return (purged: r.purged.toInt(), channel: r.channel);
|
return (purged: r.purged.toInt(), channel: r.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,8 +423,9 @@ class HubService {
|
||||||
/// Replace the operator's `default_scope:` and hot-swap on the
|
/// Replace the operator's `default_scope:` and hot-swap on the
|
||||||
/// running hub. Caller passes the new ordered list; hub rejects
|
/// running hub. Caller passes the new ordered list; hub rejects
|
||||||
/// an empty list with InvalidArgument.
|
/// an empty list with InvalidArgument.
|
||||||
Future<({List<String> scope, List<String> knownProviders})>
|
Future<({List<String> scope, List<String> knownProviders})> setDefaultScope(
|
||||||
setDefaultScope(List<String> scope) async {
|
List<String> scope,
|
||||||
|
) async {
|
||||||
return _client.setDefaultScope(scope);
|
return _client.setDefaultScope(scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,7 +435,8 @@ class HubService {
|
||||||
/// is installed side-by-side.
|
/// is installed side-by-side.
|
||||||
Future<List<String>> installedVersions(String moduleName) async {
|
Future<List<String>> installedVersions(String moduleName) async {
|
||||||
final caps = await _client.listCapabilities();
|
final caps = await _client.listCapabilities();
|
||||||
final versions = caps
|
final versions =
|
||||||
|
caps
|
||||||
.where((c) => c.moduleName == moduleName)
|
.where((c) => c.moduleName == moduleName)
|
||||||
.where((c) => c.kind.isEmpty || c.kind == 'wasm')
|
.where((c) => c.kind.isEmpty || c.kind == 'wasm')
|
||||||
.map((c) => c.moduleVersion)
|
.map((c) => c.moduleVersion)
|
||||||
|
|
@ -742,7 +753,9 @@ class HubService {
|
||||||
int backfill = 0,
|
int backfill = 0,
|
||||||
List<String> types = const [],
|
List<String> types = const [],
|
||||||
}) {
|
}) {
|
||||||
return _client.streamEvents(backfill: backfill, types: types).map(
|
return _client
|
||||||
|
.streamEvents(backfill: backfill, types: types)
|
||||||
|
.map(
|
||||||
(e) => AuditEvent(
|
(e) => AuditEvent(
|
||||||
eventId: e.eventId,
|
eventId: e.eventId,
|
||||||
timestamp: DateTime.tryParse(e.timestamp) ?? DateTime.now(),
|
timestamp: DateTime.tryParse(e.timestamp) ?? DateTime.now(),
|
||||||
|
|
@ -774,12 +787,9 @@ class HubService {
|
||||||
flowName: e.flowName.isEmpty ? null : e.flowName,
|
flowName: e.flowName.isEmpty ? null : e.flowName,
|
||||||
stepId: e.stepId.isEmpty ? null : e.stepId,
|
stepId: e.stepId.isEmpty ? null : e.stepId,
|
||||||
moduleName: e.moduleName.isEmpty ? null : e.moduleName,
|
moduleName: e.moduleName.isEmpty ? null : e.moduleName,
|
||||||
moduleVersion:
|
moduleVersion: e.moduleVersion.isEmpty ? null : e.moduleVersion,
|
||||||
e.moduleVersion.isEmpty ? null : e.moduleVersion,
|
invocationId: e.invocationId.isEmpty ? null : e.invocationId,
|
||||||
invocationId:
|
flowExecution: e.flowExecution.isEmpty ? null : e.flowExecution,
|
||||||
e.invocationId.isEmpty ? null : e.invocationId,
|
|
||||||
flowExecution:
|
|
||||||
e.flowExecution.isEmpty ? null : e.flowExecution,
|
|
||||||
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
||||||
error: e.error.isEmpty ? null : e.error,
|
error: e.error.isEmpty ? null : e.error,
|
||||||
detail: e.detail.isEmpty ? null : e.detail,
|
detail: e.detail.isEmpty ? null : e.detail,
|
||||||
|
|
@ -826,8 +836,7 @@ class HubService {
|
||||||
flowName: e.flowName,
|
flowName: e.flowName,
|
||||||
stepId: e.stepId,
|
stepId: e.stepId,
|
||||||
prompt: e.prompt,
|
prompt: e.prompt,
|
||||||
payloadPreview:
|
payloadPreview: e.payloadPreview.isEmpty ? null : e.payloadPreview,
|
||||||
e.payloadPreview.isEmpty ? null : e.payloadPreview,
|
|
||||||
createdAt: DateTime.tryParse(e.createdAt) ?? DateTime.now(),
|
createdAt: DateTime.tryParse(e.createdAt) ?? DateTime.now(),
|
||||||
expiresAt: e.expiresAt.isEmpty
|
expiresAt: e.expiresAt.isEmpty
|
||||||
? null
|
? null
|
||||||
|
|
@ -847,26 +856,19 @@ class HubService {
|
||||||
_client.approve(approvalId: id, reviewer: reviewer);
|
_client.approve(approvalId: id, reviewer: reviewer);
|
||||||
|
|
||||||
Future<void> reject(String id, String reviewer, String reason) =>
|
Future<void> reject(String id, String reviewer, String reason) =>
|
||||||
_client.reject(
|
_client.reject(approvalId: id, reviewer: reviewer, reason: reason);
|
||||||
approvalId: id,
|
|
||||||
reviewer: reviewer,
|
|
||||||
reason: reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Composite "doctor" snapshot. One round-trip per piece, run
|
/// Composite "doctor" snapshot. One round-trip per piece, run
|
||||||
/// in parallel so the page populates fast.
|
/// in parallel so the page populates fast.
|
||||||
Future<DoctorSnapshot> doctor() async {
|
Future<DoctorSnapshot> doctor() async {
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
_client.listCapabilities().catchError((_) => <CapabilityEntry>[]),
|
_client.listCapabilities().catchError((_) => <CapabilityEntry>[]),
|
||||||
_client.listApprovals(statuses: const ['pending'])
|
_client
|
||||||
|
.listApprovals(statuses: const ['pending'])
|
||||||
.catchError((_) => <PendingApprovalEntry>[]),
|
.catchError((_) => <PendingApprovalEntry>[]),
|
||||||
_client.verifyEventChain().catchError(
|
_client.verifyEventChain().catchError((_) => VerifyEventChainResponse()),
|
||||||
(_) => VerifyEventChainResponse(),
|
|
||||||
),
|
|
||||||
_client.listServices().catchError((_) => <DeclaredService>[]),
|
_client.listServices().catchError((_) => <DeclaredService>[]),
|
||||||
_client.checkUpdate().catchError(
|
_client.checkUpdate().catchError((_) => CheckUpdateResponse()),
|
||||||
(_) => CheckUpdateResponse(),
|
|
||||||
),
|
|
||||||
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -896,15 +898,11 @@ class HubService {
|
||||||
pendingApprovals: approvals.length,
|
pendingApprovals: approvals.length,
|
||||||
eventChainTotal: chain.total.toInt(),
|
eventChainTotal: chain.total.toInt(),
|
||||||
eventChainVerified: chain.verified.toInt(),
|
eventChainVerified: chain.verified.toInt(),
|
||||||
eventChainTamperedAt:
|
eventChainTamperedAt: chain.tamperedAt.isEmpty ? null : chain.tamperedAt,
|
||||||
chain.tamperedAt.isEmpty ? null : chain.tamperedAt,
|
|
||||||
services: services
|
services: services
|
||||||
.map(
|
.map(
|
||||||
(s) => ServiceEntry(
|
(s) =>
|
||||||
name: s.name,
|
ServiceEntry(name: s.name, endpoint: s.endpoint, tags: s.tags),
|
||||||
endpoint: s.endpoint,
|
|
||||||
tags: s.tags,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
update: UpdateStatus(
|
update: UpdateStatus(
|
||||||
|
|
@ -913,8 +911,9 @@ class HubService {
|
||||||
latestVersion: update.latestVersion,
|
latestVersion: update.latestVersion,
|
||||||
updateAvailable: update.updateAvailable,
|
updateAvailable: update.updateAvailable,
|
||||||
manifestReachable: update.manifestReachable,
|
manifestReachable: update.manifestReachable,
|
||||||
releaseNotesUrl:
|
releaseNotesUrl: update.releaseNotesUrl.isEmpty
|
||||||
update.releaseNotesUrl.isEmpty ? null : update.releaseNotesUrl,
|
? null
|
||||||
|
: update.releaseNotesUrl,
|
||||||
reason: update.reason.isEmpty ? null : update.reason,
|
reason: update.reason.isEmpty ? null : update.reason,
|
||||||
),
|
),
|
||||||
paths: DaemonPathsSnapshot(
|
paths: DaemonPathsSnapshot(
|
||||||
|
|
@ -958,9 +957,11 @@ class DoctorSnapshot {
|
||||||
final String? eventChainTamperedAt;
|
final String? eventChainTamperedAt;
|
||||||
final List<ServiceEntry> services;
|
final List<ServiceEntry> services;
|
||||||
final UpdateStatus update;
|
final UpdateStatus update;
|
||||||
|
|
||||||
/// Filesystem paths the daemon owns. Empty fields when the
|
/// Filesystem paths the daemon owns. Empty fields when the
|
||||||
/// hub couldn't resolve them (older daemon, no $HOME).
|
/// hub couldn't resolve them (older daemon, no $HOME).
|
||||||
final DaemonPathsSnapshot paths;
|
final DaemonPathsSnapshot paths;
|
||||||
|
|
||||||
/// Per-source-kind capability count: bundle / system / mcp /
|
/// Per-source-kind capability count: bundle / system / mcp /
|
||||||
/// n8n / temporal / webhook (0.12+). Pre-0.12 hubs return
|
/// n8n / temporal / webhook (0.12+). Pre-0.12 hubs return
|
||||||
/// an empty map and the panel falls back to a flat count.
|
/// an empty map and the panel falls back to a flat count.
|
||||||
|
|
@ -1023,14 +1024,17 @@ class CapabilityInfo {
|
||||||
final String version;
|
final String version;
|
||||||
final String moduleName;
|
final String moduleName;
|
||||||
final String kind;
|
final String kind;
|
||||||
|
|
||||||
/// Publisher segment of the fully-qualified identifier.
|
/// Publisher segment of the fully-qualified identifier.
|
||||||
/// "fai" for legacy bundles, "fai.system" for built-ins,
|
/// "fai" for legacy bundles, "fai.system" for built-ins,
|
||||||
/// operator-config service name for federated capabilities.
|
/// operator-config service name for federated capabilities.
|
||||||
final String provider;
|
final String provider;
|
||||||
|
|
||||||
/// Transport that delivers the capability: bundle / system /
|
/// Transport that delivers the capability: bundle / system /
|
||||||
/// mcp / n8n / temporal / webhook. Empty string falls back
|
/// mcp / n8n / temporal / webhook. Empty string falls back
|
||||||
/// to deriving from `kind` for pre-0.12 hubs.
|
/// to deriving from `kind` for pre-0.12 hubs.
|
||||||
final String sourceKind;
|
final String sourceKind;
|
||||||
|
|
||||||
/// Whether the version on this capability is a label rather
|
/// Whether the version on this capability is a label rather
|
||||||
/// than a hard contract. True for federation sources whose
|
/// than a hard contract. True for federation sources whose
|
||||||
/// upstream service does not honour semver.
|
/// upstream service does not honour semver.
|
||||||
|
|
@ -1065,6 +1069,7 @@ class SavedFlow {
|
||||||
final String name;
|
final String name;
|
||||||
final String path;
|
final String path;
|
||||||
final int sizeBytes;
|
final int sizeBytes;
|
||||||
|
|
||||||
/// Verbatim `use:` strings of every step in this flow,
|
/// Verbatim `use:` strings of every step in this flow,
|
||||||
/// deduplicated and sorted by the hub. Lets Studio decide
|
/// deduplicated and sorted by the hub. Lets Studio decide
|
||||||
/// whether the flow is currently runnable (every entry
|
/// whether the flow is currently runnable (every entry
|
||||||
|
|
@ -1085,11 +1090,24 @@ class ModuleDetail {
|
||||||
final List<String> capabilities;
|
final List<String> capabilities;
|
||||||
final List<String> permissions;
|
final List<String> permissions;
|
||||||
final String directory;
|
final String directory;
|
||||||
|
|
||||||
/// MIME allow-list declared in the module's `module.yaml`.
|
/// MIME allow-list declared in the module's `module.yaml`.
|
||||||
/// Empty when the module didn't declare any — caller falls
|
/// Empty when the module didn't declare any — caller falls
|
||||||
/// back to its built-in extension heuristic.
|
/// back to its built-in extension heuristic.
|
||||||
final List<String> acceptsMime;
|
final List<String> acceptsMime;
|
||||||
|
|
||||||
|
/// Declared inputs (schema v3 verbose form when present). The
|
||||||
|
/// flow editor uses these to render per-field input ports with
|
||||||
|
/// canonical names + i18n tooltips.
|
||||||
|
final List<ModuleFieldInfo> inputs;
|
||||||
|
|
||||||
|
/// Declared outputs (schema v3 verbose form when present).
|
||||||
|
/// Rendered by the flow editor as distinct output ports so a
|
||||||
|
/// step like `summarize` exposes its `response`,
|
||||||
|
/// `model_endpoint`, `model_name`, `model_digest` separately
|
||||||
|
/// instead of fanning them all out of one anchor.
|
||||||
|
final List<ModuleFieldInfo> outputs;
|
||||||
|
|
||||||
const ModuleDetail({
|
const ModuleDetail({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.version,
|
required this.version,
|
||||||
|
|
@ -1097,9 +1115,43 @@ class ModuleDetail {
|
||||||
required this.permissions,
|
required this.permissions,
|
||||||
required this.directory,
|
required this.directory,
|
||||||
this.acceptsMime = const [],
|
this.acceptsMime = const [],
|
||||||
|
this.inputs = const [],
|
||||||
|
this.outputs = const [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One declared input or output field on a module. Mirrors
|
||||||
|
/// `proto.ModuleField`: name, type descriptor, and a locale
|
||||||
|
/// → description map for tooltips.
|
||||||
|
class ModuleFieldInfo {
|
||||||
|
/// Field name, e.g. "prompt" or "model_endpoint".
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Type descriptor (text / json / bytes / file).
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
/// Locale tag → description. Use [descriptionFor] to look up
|
||||||
|
/// with English fallback.
|
||||||
|
final Map<String, String> description;
|
||||||
|
|
||||||
|
const ModuleFieldInfo({
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
this.description = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns the description in [locale] when present, else the
|
||||||
|
/// English peer, else null. Studio passes its active locale's
|
||||||
|
/// short code (e.g. "de") and shows the result as a tooltip.
|
||||||
|
String? descriptionFor(String locale) {
|
||||||
|
final exact = description[locale];
|
||||||
|
if (exact != null && exact.isNotEmpty) return exact;
|
||||||
|
final en = description['en'];
|
||||||
|
if (en != null && en.isNotEmpty) return en;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AuditEvent {
|
class AuditEvent {
|
||||||
final String eventId;
|
final String eventId;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
@ -1112,6 +1164,7 @@ class AuditEvent {
|
||||||
final String? flowExecution;
|
final String? flowExecution;
|
||||||
final int? durationMs;
|
final int? durationMs;
|
||||||
final String? error;
|
final String? error;
|
||||||
|
|
||||||
/// Free-form JSON-string carrying event-type-specific fields.
|
/// Free-form JSON-string carrying event-type-specific fields.
|
||||||
/// Surfaced verbatim in the audit drill-down dialog.
|
/// Surfaced verbatim in the audit drill-down dialog.
|
||||||
final String? detail;
|
final String? detail;
|
||||||
|
|
@ -1163,12 +1216,15 @@ class ApprovalRecord {
|
||||||
final String? payloadPreview;
|
final String? payloadPreview;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime? expiresAt;
|
final DateTime? expiresAt;
|
||||||
|
|
||||||
/// One of: pending / approved / rejected / expired.
|
/// One of: pending / approved / rejected / expired.
|
||||||
final String status;
|
final String status;
|
||||||
final DateTime? decidedAt;
|
final DateTime? decidedAt;
|
||||||
|
|
||||||
/// Reviewer handle (e.g. "alice@studio") or "system" for
|
/// Reviewer handle (e.g. "alice@studio") or "system" for
|
||||||
/// auto-expired approvals. Empty until decided.
|
/// auto-expired approvals. Empty until decided.
|
||||||
final String decidedBy;
|
final String decidedBy;
|
||||||
|
|
||||||
/// Reject reason — empty for approve / pending / expired.
|
/// Reject reason — empty for approve / pending / expired.
|
||||||
final String reason;
|
final String reason;
|
||||||
|
|
||||||
|
|
@ -1192,9 +1248,11 @@ class SystemAiStatus {
|
||||||
final String provider;
|
final String provider;
|
||||||
final String endpoint;
|
final String endpoint;
|
||||||
final String model;
|
final String model;
|
||||||
|
|
||||||
/// off / redacted / full.
|
/// off / redacted / full.
|
||||||
final String privacyMode;
|
final String privacyMode;
|
||||||
final String apiKeyEnv;
|
final String apiKeyEnv;
|
||||||
|
|
||||||
/// Number of cached System-AI explanations currently stored.
|
/// Number of cached System-AI explanations currently stored.
|
||||||
/// Surfaced in Doctor + Settings so operators see the cache
|
/// Surfaced in Doctor + Settings so operators see the cache
|
||||||
/// earning its keep.
|
/// earning its keep.
|
||||||
|
|
@ -1215,17 +1273,22 @@ class AskAiResult {
|
||||||
/// "" on success; otherwise one of:
|
/// "" on success; otherwise one of:
|
||||||
/// disabled / env_missing / network / http / parse / empty_response.
|
/// disabled / env_missing / network / http / parse / empty_response.
|
||||||
final String errorKind;
|
final String errorKind;
|
||||||
|
|
||||||
/// On success: the assistant's reply.
|
/// On success: the assistant's reply.
|
||||||
/// On failure: a human-readable diagnostic.
|
/// On failure: a human-readable diagnostic.
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
/// Round-trip latency in ms (success only). For cache hits
|
/// Round-trip latency in ms (success only). For cache hits
|
||||||
/// this is the *original* generation latency.
|
/// this is the *original* generation latency.
|
||||||
final int latencyMs;
|
final int latencyMs;
|
||||||
|
|
||||||
/// True iff the answer came from the persistent cache.
|
/// True iff the answer came from the persistent cache.
|
||||||
final bool cached;
|
final bool cached;
|
||||||
|
|
||||||
/// ISO-8601 timestamp the cached entry was first generated.
|
/// ISO-8601 timestamp the cached entry was first generated.
|
||||||
/// Empty for fresh answers.
|
/// Empty for fresh answers.
|
||||||
final String cachedAt;
|
final String cachedAt;
|
||||||
|
|
||||||
/// How many times this entry has been served from cache.
|
/// How many times this entry has been served from cache.
|
||||||
/// 0 for fresh answers.
|
/// 0 for fresh answers.
|
||||||
final int cacheHits;
|
final int cacheHits;
|
||||||
|
|
@ -1283,10 +1346,7 @@ class ChannelStatusSnapshot {
|
||||||
final String active;
|
final String active;
|
||||||
final List<ChannelInfo> channels;
|
final List<ChannelInfo> channels;
|
||||||
|
|
||||||
const ChannelStatusSnapshot({
|
const ChannelStatusSnapshot({required this.active, required this.channels});
|
||||||
required this.active,
|
|
||||||
required this.channels,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One configured n8n endpoint plus the last-discovery
|
/// One configured n8n endpoint plus the last-discovery
|
||||||
|
|
@ -1320,13 +1380,16 @@ class McpClientInfo {
|
||||||
final String endpoint;
|
final String endpoint;
|
||||||
final String apiKeyEnv;
|
final String apiKeyEnv;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
/// Number of tools the last discovery returned. 0 before
|
/// Number of tools the last discovery returned. 0 before
|
||||||
/// discovery has run or on failure.
|
/// discovery has run or on failure.
|
||||||
final int toolCount;
|
final int toolCount;
|
||||||
|
|
||||||
/// Empty on success; otherwise one of:
|
/// Empty on success; otherwise one of:
|
||||||
/// invalid_endpoint / not_implemented / network / http /
|
/// invalid_endpoint / not_implemented / network / http /
|
||||||
/// server / parse / env_missing.
|
/// server / parse / env_missing.
|
||||||
final String errorKind;
|
final String errorKind;
|
||||||
|
|
||||||
/// Human-readable diagnostic.
|
/// Human-readable diagnostic.
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
|
@ -1375,6 +1438,7 @@ class HardwareSnapshot {
|
||||||
final int ramMb;
|
final int ramMb;
|
||||||
final bool appleSilicon;
|
final bool appleSilicon;
|
||||||
final bool dedicatedGpu;
|
final bool dedicatedGpu;
|
||||||
|
|
||||||
/// Human-readable summary the editor renders verbatim.
|
/// Human-readable summary the editor renders verbatim.
|
||||||
final String summary;
|
final String summary;
|
||||||
|
|
||||||
|
|
@ -1414,8 +1478,10 @@ class CuratedModelInfo {
|
||||||
final String id;
|
final String id;
|
||||||
final String family;
|
final String family;
|
||||||
final double paramsB;
|
final double paramsB;
|
||||||
|
|
||||||
/// basic / good / excellent.
|
/// basic / good / excellent.
|
||||||
final String qualityTier;
|
final String qualityTier;
|
||||||
|
|
||||||
/// tiny / small / balanced / large.
|
/// tiny / small / balanced / large.
|
||||||
final String minHwTier;
|
final String minHwTier;
|
||||||
final int contextK;
|
final int contextK;
|
||||||
|
|
@ -1456,20 +1522,19 @@ class CuratedSnapshot {
|
||||||
final String lastReviewed;
|
final String lastReviewed;
|
||||||
final List<CuratedModelInfo> models;
|
final List<CuratedModelInfo> models;
|
||||||
|
|
||||||
const CuratedSnapshot({
|
const CuratedSnapshot({required this.lastReviewed, required this.models});
|
||||||
required this.lastReviewed,
|
|
||||||
required this.models,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row from the store-index search.
|
/// One row from the store-index search.
|
||||||
class StoreItem {
|
class StoreItem {
|
||||||
final String name;
|
final String name;
|
||||||
final String taglineEn;
|
final String taglineEn;
|
||||||
|
|
||||||
/// German tagline. Falls back to [taglineEn] in the UI when
|
/// German tagline. Falls back to [taglineEn] in the UI when
|
||||||
/// the index entry does not ship a localized one.
|
/// the index entry does not ship a localized one.
|
||||||
final String taglineDe;
|
final String taglineDe;
|
||||||
final String descriptionEn;
|
final String descriptionEn;
|
||||||
|
|
||||||
/// German long-form description. Falls back to [descriptionEn]
|
/// German long-form description. Falls back to [descriptionEn]
|
||||||
/// when missing.
|
/// when missing.
|
||||||
final String descriptionDe;
|
final String descriptionDe;
|
||||||
|
|
@ -1480,30 +1545,38 @@ class StoreItem {
|
||||||
final String license;
|
final String license;
|
||||||
final String repository;
|
final String repository;
|
||||||
final String bestVersion;
|
final String bestVersion;
|
||||||
|
|
||||||
/// "published", "alpha", "planned", or empty when unknown.
|
/// "published", "alpha", "planned", or empty when unknown.
|
||||||
final String status;
|
final String status;
|
||||||
final bool installed;
|
final bool installed;
|
||||||
|
|
||||||
/// True iff the store-index marks this entry as editorially
|
/// True iff the store-index marks this entry as editorially
|
||||||
/// featured (curated quarterly in the bundled seed.yaml).
|
/// featured (curated quarterly in the bundled seed.yaml).
|
||||||
/// Drives the "Featured" strip at the top of the Store page.
|
/// Drives the "Featured" strip at the top of the Store page.
|
||||||
final bool featured;
|
final bool featured;
|
||||||
|
|
||||||
/// Optional icon URL — square PNG / SVG. Empty for entries
|
/// Optional icon URL — square PNG / SVG. Empty for entries
|
||||||
/// that don't supply one; UI falls back to a category icon.
|
/// that don't supply one; UI falls back to a category icon.
|
||||||
final String iconUrl;
|
final String iconUrl;
|
||||||
|
|
||||||
/// Ordered list of screenshot URLs; empty when none. Studio
|
/// Ordered list of screenshot URLs; empty when none. Studio
|
||||||
/// renders them as a horizontally-scrollable strip in the
|
/// renders them as a horizontally-scrollable strip in the
|
||||||
/// detail sheet.
|
/// detail sheet.
|
||||||
final List<String> screenshotUrls;
|
final List<String> screenshotUrls;
|
||||||
|
|
||||||
/// Explicit docs URL override. When empty, the hub falls back
|
/// Explicit docs URL override. When empty, the hub falls back
|
||||||
/// to the well-known raw-README paths off [repository].
|
/// to the well-known raw-README paths off [repository].
|
||||||
final String docsUrl;
|
final String docsUrl;
|
||||||
|
|
||||||
/// "native" (downloadable bundle) or "federated" (routes
|
/// "native" (downloadable bundle) or "federated" (routes
|
||||||
/// through a bridge — MCP, n8n, …). Empty falls back to
|
/// through a bridge — MCP, n8n, …). Empty falls back to
|
||||||
/// "native".
|
/// "native".
|
||||||
final String kind;
|
final String kind;
|
||||||
|
|
||||||
/// Provider id for federated entries. Empty for native.
|
/// Provider id for federated entries. Empty for native.
|
||||||
/// E.g. "filesystem" for an `mcp.filesystem.read_file` tool.
|
/// E.g. "filesystem" for an `mcp.filesystem.read_file` tool.
|
||||||
final String provider;
|
final String provider;
|
||||||
|
|
||||||
/// Transport delivering this entry. Distinct from `kind` —
|
/// Transport delivering this entry. Distinct from `kind` —
|
||||||
/// `sourceKind` answers "HOW does the hub reach this", not
|
/// `sourceKind` answers "HOW does the hub reach this", not
|
||||||
/// "is it installable". Values: "bundle", "mcp", "n8n",
|
/// "is it installable". Values: "bundle", "mcp", "n8n",
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,13 @@ class _StorePageState extends State<StorePage> {
|
||||||
final _queryCtrl = TextEditingController();
|
final _queryCtrl = TextEditingController();
|
||||||
String _category = '';
|
String _category = '';
|
||||||
String _status = '';
|
String _status = '';
|
||||||
|
|
||||||
/// Source filter: '' (all), 'native', 'mcp', 'n8n'. Applied
|
/// Source filter: '' (all), 'native', 'mcp', 'n8n'. Applied
|
||||||
/// client-side after the hub returns results — the search RPC
|
/// client-side after the hub returns results — the search RPC
|
||||||
/// has no source field.
|
/// has no source field.
|
||||||
String _source = '';
|
String _source = '';
|
||||||
bool _installedOnly = false;
|
bool _installedOnly = false;
|
||||||
|
|
||||||
/// "Show installable modules only". On by default: roughly
|
/// "Show installable modules only". On by default: roughly
|
||||||
/// 2/3 of the seed-index entries currently carry status
|
/// 2/3 of the seed-index entries currently carry status
|
||||||
/// `planned` (no bundle to install), and pre-filter polish
|
/// `planned` (no bundle to install), and pre-filter polish
|
||||||
|
|
@ -43,20 +45,24 @@ class _StorePageState extends State<StorePage> {
|
||||||
/// install half the cards. Planned modules are one chip-flip
|
/// install half the cards. Planned modules are one chip-flip
|
||||||
/// away in the filter dialog.
|
/// away in the filter dialog.
|
||||||
bool _installableOnly = true;
|
bool _installableOnly = true;
|
||||||
|
|
||||||
/// Tracks per-recommended-source add buttons to disable them
|
/// Tracks per-recommended-source add buttons to disable them
|
||||||
/// while the request is in flight.
|
/// while the request is in flight.
|
||||||
final Set<String> _addingSources = <String>{};
|
final Set<String> _addingSources = <String>{};
|
||||||
|
|
||||||
/// Whether the operator dismissed the editorial hero this
|
/// Whether the operator dismissed the editorial hero this
|
||||||
/// session. Not persisted — the next Studio launch shows it
|
/// session. Not persisted — the next Studio launch shows it
|
||||||
/// again so a release-bumped story has a chance to be seen.
|
/// again so a release-bumped story has a chance to be seen.
|
||||||
bool _todayDismissed = false;
|
bool _todayDismissed = false;
|
||||||
|
|
||||||
/// The carousel of stories rendered in the hero. When an
|
/// The carousel of stories rendered in the hero. When an
|
||||||
/// operator-accepted story exists at `~/.fai/today/active.yaml`,
|
/// operator-accepted story exists at `~/.fai/today/active.yaml`,
|
||||||
/// it is the only entry (carousel collapses to a single
|
/// it is the only entry (carousel collapses to a single
|
||||||
/// slide). Otherwise the curated fallback list rotates.
|
/// slide). Otherwise the curated fallback list rotates.
|
||||||
late final List<TodayStoryData> _todayStories = (() {
|
late final List<TodayStoryData> _todayStories = (() {
|
||||||
final loaded =
|
final loaded = TodayStoryLoader.loadOrFallback(
|
||||||
TodayStoryLoader.loadOrFallback(_kFallbackTodayStories.first);
|
_kFallbackTodayStories.first,
|
||||||
|
);
|
||||||
// When the loader fell back to the const, it returned the
|
// When the loader fell back to the const, it returned the
|
||||||
// first entry of our list — surface the whole list so the
|
// first entry of our list — surface the whole list so the
|
||||||
// operator can browse. Otherwise the loader produced a real
|
// operator can browse. Otherwise the loader produced a real
|
||||||
|
|
@ -66,6 +72,7 @@ class _StorePageState extends State<StorePage> {
|
||||||
}
|
}
|
||||||
return <TodayStoryData>[loaded];
|
return <TodayStoryData>[loaded];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/// Carousel index. Wraps around modulo `_todayStories.length`.
|
/// Carousel index. Wraps around modulo `_todayStories.length`.
|
||||||
int _todayIndex = 0;
|
int _todayIndex = 0;
|
||||||
late Future<List<StoreItem>> _future;
|
late Future<List<StoreItem>> _future;
|
||||||
|
|
@ -82,11 +89,13 @@ class _StorePageState extends State<StorePage> {
|
||||||
List<_AiMatch>? _aiMatches;
|
List<_AiMatch>? _aiMatches;
|
||||||
Set<String>? _aiMatchedNames;
|
Set<String>? _aiMatchedNames;
|
||||||
String? _aiError;
|
String? _aiError;
|
||||||
|
|
||||||
/// Debounce-Timer für Such-Filterung beim Tippen. Ohne
|
/// Debounce-Timer für Such-Filterung beim Tippen. Ohne
|
||||||
/// Debounce flackert "Keine Treffer" zwischen Buchstaben
|
/// Debounce flackert "Keine Treffer" zwischen Buchstaben
|
||||||
/// (für 2-3 Buchstaben matched z.B. nichts, das nächste
|
/// (für 2-3 Buchstaben matched z.B. nichts, das nächste
|
||||||
/// dann doch). 250ms ist comfortable für Tippen.
|
/// dann doch). 250ms ist comfortable für Tippen.
|
||||||
Timer? _typingDebounce;
|
Timer? _typingDebounce;
|
||||||
|
|
||||||
/// Whether the operator has a System-AI configured. Drives
|
/// Whether the operator has a System-AI configured. Drives
|
||||||
/// the Ask-bar hint copy and disables the AI path when off.
|
/// the Ask-bar hint copy and disables the AI path when off.
|
||||||
bool _systemAiEnabled = false;
|
bool _systemAiEnabled = false;
|
||||||
|
|
@ -151,8 +160,10 @@ class _StorePageState extends State<StorePage> {
|
||||||
// An already-installed item counts as "installable" for
|
// An already-installed item counts as "installable" for
|
||||||
// this filter — the user can still update / uninstall it.
|
// this filter — the user can still update / uninstall it.
|
||||||
out = out
|
out = out
|
||||||
.where((e) =>
|
.where(
|
||||||
e.installed || e.status == 'published' || e.status == 'alpha')
|
(e) =>
|
||||||
|
e.installed || e.status == 'published' || e.status == 'alpha',
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -292,7 +303,8 @@ class _StorePageState extends State<StorePage> {
|
||||||
// Browsing-only chrome — every editorial
|
// Browsing-only chrome — every editorial
|
||||||
// surface hides the moment a filter is set so
|
// surface hides the moment a filter is set so
|
||||||
// the operator's query stays the focus.
|
// the operator's query stays the focus.
|
||||||
final isBrowsing = _queryCtrl.text.trim().isEmpty &&
|
final isBrowsing =
|
||||||
|
_queryCtrl.text.trim().isEmpty &&
|
||||||
_category.isEmpty &&
|
_category.isEmpty &&
|
||||||
_status.isEmpty &&
|
_status.isEmpty &&
|
||||||
_source.isEmpty &&
|
_source.isEmpty &&
|
||||||
|
|
@ -312,7 +324,8 @@ class _StorePageState extends State<StorePage> {
|
||||||
// out of the way.
|
// out of the way.
|
||||||
final showStandaloneRecommended =
|
final showStandaloneRecommended =
|
||||||
isBrowsing && _todayDismissed && hasNoFederation;
|
isBrowsing && _todayDismissed && hasNoFederation;
|
||||||
final showFeatured = isBrowsing &&
|
final showFeatured =
|
||||||
|
isBrowsing &&
|
||||||
_todayDismissed &&
|
_todayDismissed &&
|
||||||
items.any((e) => e.featured);
|
items.any((e) => e.featured);
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|
@ -334,13 +347,15 @@ class _StorePageState extends State<StorePage> {
|
||||||
if (showToday) ...[
|
if (showToday) ...[
|
||||||
_StoreTodayHero(
|
_StoreTodayHero(
|
||||||
story:
|
story:
|
||||||
_todayStories[_todayIndex % _todayStories.length],
|
_todayStories[_todayIndex %
|
||||||
|
_todayStories.length],
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
currentIndex: _todayIndex,
|
currentIndex: _todayIndex,
|
||||||
totalCount: _todayStories.length,
|
totalCount: _todayStories.length,
|
||||||
onPrev: _todayStories.length > 1
|
onPrev: _todayStories.length > 1
|
||||||
? () => setState(() {
|
? () => setState(() {
|
||||||
_todayIndex = (_todayIndex -
|
_todayIndex =
|
||||||
|
(_todayIndex -
|
||||||
1 +
|
1 +
|
||||||
_todayStories.length) %
|
_todayStories.length) %
|
||||||
_todayStories.length;
|
_todayStories.length;
|
||||||
|
|
@ -348,14 +363,16 @@ class _StorePageState extends State<StorePage> {
|
||||||
: null,
|
: null,
|
||||||
onNext: _todayStories.length > 1
|
onNext: _todayStories.length > 1
|
||||||
? () => setState(() {
|
? () => setState(() {
|
||||||
_todayIndex = (_todayIndex + 1) %
|
_todayIndex =
|
||||||
|
(_todayIndex + 1) %
|
||||||
_todayStories.length;
|
_todayStories.length;
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
onDismiss: () =>
|
onDismiss: () =>
|
||||||
setState(() => _todayDismissed = true),
|
setState(() => _todayDismissed = true),
|
||||||
onCta: _todayStories[
|
onCta:
|
||||||
_todayIndex % _todayStories.length]
|
_todayStories[_todayIndex %
|
||||||
|
_todayStories.length]
|
||||||
.cta ==
|
.cta ==
|
||||||
TodayCta.openSettings
|
TodayCta.openSettings
|
||||||
? () => FaiSettingsDialog.show(context)
|
? () => FaiSettingsDialog.show(context)
|
||||||
|
|
@ -538,13 +555,15 @@ class _StorePageState extends State<StorePage> {
|
||||||
try {
|
try {
|
||||||
final all = await HubService.instance.searchStore(limit: 200);
|
final all = await HubService.instance.searchStore(limit: 200);
|
||||||
final modules = all
|
final modules = all
|
||||||
.map((s) => {
|
.map(
|
||||||
|
(s) => {
|
||||||
'name': s.name,
|
'name': s.name,
|
||||||
'tagline': _locale == 'de' && s.taglineDe.isNotEmpty
|
'tagline': _locale == 'de' && s.taglineDe.isNotEmpty
|
||||||
? s.taglineDe
|
? s.taglineDe
|
||||||
: s.taglineEn,
|
: s.taglineEn,
|
||||||
'category': s.category,
|
'category': s.category,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final prompt = _buildAiSearchPrompt(query, modules, _locale);
|
final prompt = _buildAiSearchPrompt(query, modules, _locale);
|
||||||
final result = await HubService.instance.askAi(prompt);
|
final result = await HubService.instance.askAi(prompt);
|
||||||
|
|
@ -794,12 +813,14 @@ class _AskBar extends StatelessWidget {
|
||||||
// can write multi-line questions naturally.
|
// can write multi-line questions naturally.
|
||||||
child: Shortcuts(
|
child: Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.meta,
|
LogicalKeySet(
|
||||||
LogicalKeyboardKey.enter):
|
LogicalKeyboardKey.meta,
|
||||||
const _SubmitAskIntent(),
|
LogicalKeyboardKey.enter,
|
||||||
LogicalKeySet(LogicalKeyboardKey.control,
|
): const _SubmitAskIntent(),
|
||||||
LogicalKeyboardKey.enter):
|
LogicalKeySet(
|
||||||
const _SubmitAskIntent(),
|
LogicalKeyboardKey.control,
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
): const _SubmitAskIntent(),
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: <Type, Action<Intent>>{
|
actions: <Type, Action<Intent>>{
|
||||||
|
|
@ -822,10 +843,10 @@ class _AskBar extends StatelessWidget {
|
||||||
// Cmd/Ctrl+Enter.
|
// Cmd/Ctrl+Enter.
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
const EdgeInsets.symmetric(vertical: 6),
|
hintText: aiAvailable
|
||||||
hintText:
|
? l.storeAskHint
|
||||||
aiAvailable ? l.storeAskHint : l.storeSearchHint,
|
: l.storeSearchHint,
|
||||||
hintMaxLines: 2,
|
hintMaxLines: 2,
|
||||||
hintStyle: theme.textTheme.bodyMedium?.copyWith(
|
hintStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -857,9 +878,7 @@ class _AskBar extends StatelessWidget {
|
||||||
aiAvailable ? Icons.auto_awesome : Icons.search,
|
aiAvailable ? Icons.auto_awesome : Icons.search,
|
||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(thinking ? l.storeAskAiThinking : l.storeAskSubmit),
|
||||||
thinking ? l.storeAskAiThinking : l.storeAskSubmit,
|
|
||||||
),
|
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
|
|
@ -883,10 +902,7 @@ class _FilterButton extends StatelessWidget {
|
||||||
final int activeCount;
|
final int activeCount;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
const _FilterButton({
|
const _FilterButton({required this.activeCount, required this.onPressed});
|
||||||
required this.activeCount,
|
|
||||||
required this.onPressed,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -902,10 +918,7 @@ class _FilterButton extends StatelessWidget {
|
||||||
if (activeCount > 0) ...[
|
if (activeCount > 0) ...[
|
||||||
const SizedBox(width: FaiSpace.xs),
|
const SizedBox(width: FaiSpace.xs),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||||
horizontal: 6,
|
|
||||||
vertical: 1,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary.withValues(alpha: 0.18),
|
color: theme.colorScheme.primary.withValues(alpha: 0.18),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|
@ -921,9 +934,7 @@ class _FilterButton extends StatelessWidget {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -986,9 +997,11 @@ class _FilterDialogState extends State<_FilterDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Widget chipRow(List<({String value, String label})> items, String selected,
|
Widget chipRow(
|
||||||
void Function(String) onSelect) =>
|
List<({String value, String label})> items,
|
||||||
Wrap(
|
String selected,
|
||||||
|
void Function(String) onSelect,
|
||||||
|
) => Wrap(
|
||||||
spacing: FaiSpace.xs,
|
spacing: FaiSpace.xs,
|
||||||
runSpacing: FaiSpace.xs,
|
runSpacing: FaiSpace.xs,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -1279,10 +1292,7 @@ class _CategoryDropdown extends StatelessWidget {
|
||||||
initialValue: selected,
|
initialValue: selected,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(value: '', child: Text(l.storeCategoryAll)),
|
||||||
value: '',
|
|
||||||
child: Text(l.storeCategoryAll),
|
|
||||||
),
|
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
for (final c in categories)
|
for (final c in categories)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
|
@ -1419,6 +1429,7 @@ class _ChoiceChip extends StatelessWidget {
|
||||||
class _StoreGrid extends StatelessWidget {
|
class _StoreGrid extends StatelessWidget {
|
||||||
final List<StoreItem> items;
|
final List<StoreItem> items;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
|
||||||
/// Map of installed module name → installed version. Cards
|
/// Map of installed module name → installed version. Cards
|
||||||
/// compare against `bestVersion` to render an "Update" badge
|
/// compare against `bestVersion` to render an "Update" badge
|
||||||
/// when the store offers something newer.
|
/// when the store offers something newer.
|
||||||
|
|
@ -1594,8 +1605,9 @@ class _FeaturedTile extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor:
|
backgroundColor: theme.colorScheme.primary.withValues(
|
||||||
theme.colorScheme.primary.withValues(alpha: 0.18),
|
alpha: 0.18,
|
||||||
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_iconForCategory(item.category),
|
_iconForCategory(item.category),
|
||||||
size: 26,
|
size: 26,
|
||||||
|
|
@ -1720,6 +1732,7 @@ class _FeaturedTile extends StatelessWidget {
|
||||||
class _StoreCard extends StatelessWidget {
|
class _StoreCard extends StatelessWidget {
|
||||||
final StoreItem item;
|
final StoreItem item;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
|
||||||
/// Installed version for this name, when the operator has it.
|
/// Installed version for this name, when the operator has it.
|
||||||
/// `null` for not-installed entries.
|
/// `null` for not-installed entries.
|
||||||
final String? installedVersion;
|
final String? installedVersion;
|
||||||
|
|
@ -1775,8 +1788,9 @@ class _StoreCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 18,
|
radius: 18,
|
||||||
backgroundColor:
|
backgroundColor: theme.colorScheme.primary.withValues(
|
||||||
theme.colorScheme.primary.withValues(alpha: 0.12),
|
alpha: 0.12,
|
||||||
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_iconForCategory(item.category),
|
_iconForCategory(item.category),
|
||||||
size: 18,
|
size: 18,
|
||||||
|
|
@ -1944,6 +1958,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
String get _locale => Localizations.localeOf(context).languageCode;
|
String get _locale => Localizations.localeOf(context).languageCode;
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
String? _toast;
|
String? _toast;
|
||||||
|
|
||||||
/// `null` while the initial probe is in flight; non-null once
|
/// `null` while the initial probe is in flight; non-null once
|
||||||
/// `readInstalledModuleDocs` has returned. The probe is cheap —
|
/// `readInstalledModuleDocs` has returned. The probe is cheap —
|
||||||
/// a single file stat on disk — so we run it eagerly when the
|
/// a single file stat on disk — so we run it eagerly when the
|
||||||
|
|
@ -1951,6 +1966,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
/// behind a "Load docs" click.
|
/// behind a "Load docs" click.
|
||||||
({String state, String text, String sourcePath})? _docsResult;
|
({String state, String text, String sourcePath})? _docsResult;
|
||||||
bool _docsProbed = false;
|
bool _docsProbed = false;
|
||||||
|
|
||||||
/// Live module-info for installed entries: declared
|
/// Live module-info for installed entries: declared
|
||||||
/// permissions and on-disk directory. Pulled lazily so
|
/// permissions and on-disk directory. Pulled lazily so
|
||||||
/// not-installed entries don't run an unnecessary RPC. Stays
|
/// not-installed entries don't run an unnecessary RPC. Stays
|
||||||
|
|
@ -1980,22 +1996,25 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
Future<void> _probeInstalledDocs() async {
|
Future<void> _probeInstalledDocs() async {
|
||||||
final locale = Localizations.localeOf(context).languageCode;
|
final locale = Localizations.localeOf(context).languageCode;
|
||||||
try {
|
try {
|
||||||
final r = await HubService.instance
|
final r = await HubService.instance.readInstalledModuleDocs(
|
||||||
.readInstalledModuleDocs(widget.item.name, locale: locale);
|
widget.item.name,
|
||||||
|
locale: locale,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _docsResult = r);
|
setState(() => _docsResult = r);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Treat probe failures as "no docs available" — the section
|
// Treat probe failures as "no docs available" — the section
|
||||||
// simply hides, the rest of the sheet still works.
|
// simply hides, the rest of the sheet still works.
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _docsResult = (state: 'no_docs', text: '', sourcePath: ''));
|
setState(
|
||||||
|
() => _docsResult = (state: 'no_docs', text: '', sourcePath: ''),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadModuleDetail() async {
|
Future<void> _loadModuleDetail() async {
|
||||||
try {
|
try {
|
||||||
final detail =
|
final detail = await HubService.instance.moduleInfo(widget.item.name);
|
||||||
await HubService.instance.moduleInfo(widget.item.name);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _moduleDetail = detail);
|
setState(() => _moduleDetail = detail);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -2009,7 +2028,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
_toast = null;
|
_toast = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final r = await HubService.instance.installModule(source: widget.item.name);
|
final r = await HubService.instance.installModule(
|
||||||
|
source: widget.item.name,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -2033,8 +2054,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
// more than one is installed side-by-side. Otherwise the
|
// more than one is installed side-by-side. Otherwise the
|
||||||
// hub picks "highest version" and the operator can be
|
// hub picks "highest version" and the operator can be
|
||||||
// surprised by what disappeared.
|
// surprised by what disappeared.
|
||||||
final versions =
|
final versions = await HubService.instance.installedVersions(
|
||||||
await HubService.instance.installedVersions(widget.item.name);
|
widget.item.name,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
String? targetVersion;
|
String? targetVersion;
|
||||||
if (versions.length > 1) {
|
if (versions.length > 1) {
|
||||||
|
|
@ -2189,13 +2211,17 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
if (item.status.isNotEmpty)
|
if (item.status.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: _statusDisplayName(
|
label: _statusDisplayName(
|
||||||
context, item.status),
|
context,
|
||||||
|
item.status,
|
||||||
|
),
|
||||||
tone: _toneForStatus(item.status),
|
tone: _toneForStatus(item.status),
|
||||||
),
|
),
|
||||||
if (item.category.isNotEmpty)
|
if (item.category.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: _categoryDisplayName(
|
label: _categoryDisplayName(
|
||||||
context, item.category),
|
context,
|
||||||
|
item.category,
|
||||||
|
),
|
||||||
tone: FaiPillTone.neutral,
|
tone: FaiPillTone.neutral,
|
||||||
),
|
),
|
||||||
if (item.installed)
|
if (item.installed)
|
||||||
|
|
@ -2322,8 +2348,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
Icon(
|
Icon(
|
||||||
_iconForPermission(p),
|
_iconForPermission(p),
|
||||||
size: 14,
|
size: 14,
|
||||||
color:
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
|
|
@ -2360,9 +2385,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
itemCount: item.screenshotUrls.length,
|
itemCount: item.screenshotUrls.length,
|
||||||
separatorBuilder: (_, _) =>
|
separatorBuilder: (_, _) =>
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
itemBuilder: (context, i) => _Screenshot(
|
itemBuilder: (context, i) =>
|
||||||
url: item.screenshotUrls[i],
|
_Screenshot(url: item.screenshotUrls[i]),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
|
@ -2424,16 +2448,18 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
child:
|
child: CircularProgressIndicator(
|
||||||
CircularProgressIndicator(strokeWidth: 2),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.delete_outline, size: 16),
|
: const Icon(Icons.delete_outline, size: 16),
|
||||||
label: Text(l.buttonUninstall),
|
label: Text(l.buttonUninstall),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: theme.colorScheme.error,
|
foregroundColor: theme.colorScheme.error,
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: theme.colorScheme.error
|
color: theme.colorScheme.error.withValues(
|
||||||
.withValues(alpha: 0.5),
|
alpha: 0.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -2444,8 +2470,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
child:
|
child: CircularProgressIndicator(
|
||||||
CircularProgressIndicator(strokeWidth: 2),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.download, size: 16),
|
: const Icon(Icons.download, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
|
|
@ -2480,10 +2507,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
l.storeNotInstallableInline(item.status),
|
l.storeNotInstallableInline(item.status),
|
||||||
style:
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
theme.textTheme.bodySmall?.copyWith(
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
color:
|
|
||||||
theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -2676,9 +2701,13 @@ class _DocsPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
// No maxHeight + NeverScrollableScrollPhysics so the docs
|
||||||
|
// section participates in the outer sheet's scroll instead
|
||||||
|
// of opening its own viewport. The previous nested-scroll
|
||||||
|
// form trapped the reader on a 480 px window with two
|
||||||
|
// scrollbars fighting for the wheel.
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
constraints: const BoxConstraints(maxHeight: 480),
|
|
||||||
padding: const EdgeInsets.all(FaiSpace.md),
|
padding: const EdgeInsets.all(FaiSpace.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
|
@ -2688,6 +2717,7 @@ class _DocsPanel extends StatelessWidget {
|
||||||
child: Markdown(
|
child: Markdown(
|
||||||
data: text,
|
data: text,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
selectable: true,
|
selectable: true,
|
||||||
onTapLink: (text, href, title) async {
|
onTapLink: (text, href, title) async {
|
||||||
if (href == null || href.isEmpty) return;
|
if (href == null || href.isEmpty) return;
|
||||||
|
|
@ -2926,6 +2956,7 @@ const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
|
||||||
class _StoreTodayHero extends StatelessWidget {
|
class _StoreTodayHero extends StatelessWidget {
|
||||||
final TodayStoryData story;
|
final TodayStoryData story;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
|
||||||
/// Carousel position (0-based) and total slide count. Drives
|
/// Carousel position (0-based) and total slide count. Drives
|
||||||
/// the dot indicator and the visibility of the prev/next
|
/// the dot indicator and the visibility of the prev/next
|
||||||
/// arrows. When `totalCount == 1` the chrome collapses to a
|
/// arrows. When `totalCount == 1` the chrome collapses to a
|
||||||
|
|
@ -2936,10 +2967,12 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
final VoidCallback? onPrev;
|
final VoidCallback? onPrev;
|
||||||
final VoidCallback? onNext;
|
final VoidCallback? onNext;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
/// CTA action — null hides the button. Wired by the parent
|
/// CTA action — null hides the button. Wired by the parent
|
||||||
/// because navigation targets live in the store-page state,
|
/// because navigation targets live in the store-page state,
|
||||||
/// not in const story data.
|
/// not in const story data.
|
||||||
final VoidCallback? onCta;
|
final VoidCallback? onCta;
|
||||||
|
|
||||||
/// Inline one-click "+ Add public source" chips rendered in
|
/// Inline one-click "+ Add public source" chips rendered in
|
||||||
/// the hero footer. Empty list hides the row — the parent
|
/// the hero footer. Empty list hides the row — the parent
|
||||||
/// passes [_kRecommendedSources] only when the store has zero
|
/// passes [_kRecommendedSources] only when the store has zero
|
||||||
|
|
@ -3011,11 +3044,7 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
color: theme.colorScheme.primary.withValues(alpha: 0.14),
|
color: theme.colorScheme.primary.withValues(alpha: 0.14),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(story.icon, size: 24, color: theme.colorScheme.primary),
|
||||||
story.icon,
|
|
||||||
size: 24,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.lg),
|
const SizedBox(width: FaiSpace.lg),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -3027,10 +3056,7 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaiPill(
|
FaiPill(label: l.storeTodayBadge, tone: FaiPillTone.accent),
|
||||||
label: l.storeTodayBadge,
|
|
||||||
tone: FaiPillTone.accent,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
badge,
|
badge,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -3091,13 +3117,15 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
child:
|
child: CircularProgressIndicator(
|
||||||
CircularProgressIndicator(strokeWidth: 2),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: Icon(s.icon, size: 14),
|
: Icon(s.icon, size: 14),
|
||||||
label: Text('+ ${s.label}'),
|
label: Text('+ ${s.label}'),
|
||||||
onPressed:
|
onPressed:
|
||||||
recommendedAdding.contains(s.name) || onAddRecommended == null
|
recommendedAdding.contains(s.name) ||
|
||||||
|
onAddRecommended == null
|
||||||
? null
|
? null
|
||||||
: () => onAddRecommended!(s),
|
: () => onAddRecommended!(s),
|
||||||
),
|
),
|
||||||
|
|
@ -3131,9 +3159,7 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
onPressed: onPrev,
|
onPressed: onPrev,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
horizontal: 4,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -3141,9 +3167,7 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
Container(
|
Container(
|
||||||
width: 6,
|
width: 6,
|
||||||
height: 6,
|
height: 6,
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
horizontal: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: i == currentIndex
|
color: i == currentIndex
|
||||||
|
|
@ -3178,11 +3202,14 @@ class _RecommendedSource {
|
||||||
/// the prefix of every synthetic store entry the server emits
|
/// the prefix of every synthetic store entry the server emits
|
||||||
/// (`mcp.<name>.<tool>`).
|
/// (`mcp.<name>.<tool>`).
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
/// Display label shown on the card.
|
/// Display label shown on the card.
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
/// HTTPS Streamable-HTTP endpoint. No auth required.
|
/// HTTPS Streamable-HTTP endpoint. No auth required.
|
||||||
final String endpoint;
|
final String endpoint;
|
||||||
|
|
||||||
/// Bilingual description used as the MCP-client `description`
|
/// Bilingual description used as the MCP-client `description`
|
||||||
/// field — keeps the audit log in the operator's locale.
|
/// field — keeps the audit log in the operator's locale.
|
||||||
final String descriptionEn;
|
final String descriptionEn;
|
||||||
|
|
|
||||||
|
|
@ -119,16 +119,16 @@ packages:
|
||||||
path: "../fai_client_sdk_dart"
|
path: "../fai_client_sdk_dart"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.17.1"
|
version: "0.18.0"
|
||||||
fai_studio_flow_editor:
|
fai_studio_flow_editor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: main
|
ref: main
|
||||||
resolved-ref: "1f5601a461097c62140354fb0e9b9bdea598b89c"
|
resolved-ref: b1fe765468d83db97344440fcfd6848a09ec06c2
|
||||||
url: "https://git.flemming.ai/fai/studio-flow-editor"
|
url: "https://git.flemming.ai/fai/studio-flow-editor"
|
||||||
source: git
|
source: git
|
||||||
version: "0.8.0"
|
version: "0.9.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.55.0
|
version: 0.56.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue