feat(studio): MCP suggestions + Approvals/Audit/Settings i18n (v0.26.0)
- Add curated catalogue of 9 official Anthropic MCP servers as click-to-prefill chips in the Add-MCP-server dialog. Operator still confirms via "Add + discover"; nothing is auto-spawned, preserving the regulated-environment trust model. - Localize Approvals page: pending/history empty states, status pills, payload preview, approve/reject toasts, reject dialog, history detail dialog, expires-in pill. - Localize Audit page: filter chips, clear-log dialog and toasts, no-events / hub-unreachable empty states, live-status bar with pluralized event count, hash-chain-verified badge, System AI panel (cached pill, latency, regenerate tooltip, asking state), event-detail dialog actions. - Localize Settings dialog: hub-endpoint section, channel blurb + popup-menu items, System AI panel headers and off-blurb, MCP/N8N panel headers + pluralized counts + refresh/add/remove tooltips, Add-MCP and Add-n8n dialogs (intro, suggestions label, all field labels and hints, save buttons, toasts). Field labels that map directly to JSON keys (event_id, flow, step, module, provider, endpoint, model) stay English by design — they're technical identifiers, not UI copy. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
349619d68e
commit
d25b4c87ae
9 changed files with 2022 additions and 209 deletions
|
|
@ -36,6 +36,62 @@
|
||||||
"settingsSaveAndConnect": "Speichern & verbinden",
|
"settingsSaveAndConnect": "Speichern & verbinden",
|
||||||
"settingsLanguage": "Sprache",
|
"settingsLanguage": "Sprache",
|
||||||
"settingsLanguageHint": "Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.",
|
"settingsLanguageHint": "Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.",
|
||||||
|
"settingsHubEndpointHint": "Wohin soll Studio sich verbinden? Standard ist der lokale Hub auf 127.0.0.1:50051.",
|
||||||
|
"settingsTlsSubtitle": "https/grpc-secure",
|
||||||
|
"settingsPortError": "Port muss 1–65535 sein",
|
||||||
|
"channelsBlurb": "Hub-Kanal wechseln (schreibt ~/.fai/current-channel und startet den Daemon neu). Verbinden ändert nur die Wire von Studio.",
|
||||||
|
"channelsActionsTooltip": "Kanal-Aktionen",
|
||||||
|
"systemAiHeader": "SYSTEM-AI",
|
||||||
|
"systemAiEnabled": "aktiv",
|
||||||
|
"systemAiOff": "aus",
|
||||||
|
"systemAiEdit": "Bearbeiten…",
|
||||||
|
"systemAiConfigure": "Konfigurieren…",
|
||||||
|
"systemAiOffBlurb": "Aus — Fehlererklärungen auf der Protokoll-Seite sind deaktiviert. Klicke Konfigurieren…, um einen Provider zu wählen (Ollama / OpenAI / LM Studio / vLLM / Eigene). Die Operator-Konfig wird direkt überschrieben; kein Daemon-Neustart nötig.",
|
||||||
|
"mcpServersCount": "{n} {n, plural, =1{Server} other{Server}}",
|
||||||
|
"@mcpServersCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"mcpRediscoverTooltip": "Discovery neu ausführen",
|
||||||
|
"mcpAddTooltip": "MCP-Server hinzufügen",
|
||||||
|
"mcpRemoveTooltip": "Server entfernen",
|
||||||
|
"mcpToolsCount": "{n} Tools",
|
||||||
|
"@mcpToolsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nEndpointsCount": "{n} {n, plural, =1{Endpunkt} other{Endpunkte}}",
|
||||||
|
"@n8nEndpointsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nWorkflowsCount": "{n} Workflows",
|
||||||
|
"@n8nWorkflowsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nRediscoverTooltip": "Discovery neu ausführen",
|
||||||
|
"n8nAddTooltip": "n8n-Endpunkt hinzufügen",
|
||||||
|
"n8nRemoveTooltip": "Endpunkt entfernen",
|
||||||
|
"addMcpTitle": "MCP-Server hinzufügen",
|
||||||
|
"addMcpIntro": "Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Capabilities.",
|
||||||
|
"addMcpSuggestionsLabel": "Vorschläge — zum Vorausfüllen klicken",
|
||||||
|
"addMcpNameLabel": "Name (keine Punkte)",
|
||||||
|
"addMcpNameHint": "filesystem",
|
||||||
|
"addMcpEndpointLabel": "Endpunkt",
|
||||||
|
"addMcpEndpointHint": "http://127.0.0.1:3001/mcp",
|
||||||
|
"addMcpApiKeyLabel": "API-Key-Umgebungsvariable (optional)",
|
||||||
|
"addMcpApiKeyHint": "MCP_FILESYSTEM_TOKEN",
|
||||||
|
"addMcpNotesLabel": "Notizen (optional)",
|
||||||
|
"addMcpAddButton": "Hinzufügen + Discovery",
|
||||||
|
"addN8nTitle": "n8n-Endpunkt hinzufügen",
|
||||||
|
"addN8nIntro": "Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Capabilities.",
|
||||||
|
"addN8nNameLabel": "Name (keine Punkte)",
|
||||||
|
"addN8nNameHint": "ops",
|
||||||
|
"addN8nBaseUrlLabel": "Basis-URL (ohne /api/v1)",
|
||||||
|
"addN8nBaseUrlHint": "https://n8n.example.com",
|
||||||
|
"addN8nApiKeyLabel": "API-Key-Umgebungsvariable (optional)",
|
||||||
|
"addN8nApiKeyHint": "N8N_OPS_KEY",
|
||||||
|
"addN8nNotesLabel": "Notizen (optional)",
|
||||||
|
"addN8nAddButton": "Hinzufügen + Discovery",
|
||||||
|
"addedMcpToast": "MCP-Server „{name}\" hinzugefügt.",
|
||||||
|
"@addedMcpToast": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"addedN8nToast": "n8n-Endpunkt „{name}\" hinzugefügt.",
|
||||||
|
"@addedN8nToast": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"addFailedToast": "Hinzufügen fehlgeschlagen: {error}",
|
||||||
|
"@addFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"removeFailedToast": "Entfernen fehlgeschlagen: {error}",
|
||||||
|
"@removeFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"refreshFailedToast": "Aktualisieren fehlgeschlagen: {error}",
|
||||||
|
"@refreshFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
|
||||||
"channelsHeader": "KANÄLE",
|
"channelsHeader": "KANÄLE",
|
||||||
"channelsActive": "aktiv",
|
"channelsActive": "aktiv",
|
||||||
|
|
@ -68,6 +124,56 @@
|
||||||
"auditTitle": "Protokoll",
|
"auditTitle": "Protokoll",
|
||||||
"auditNoEvents": "Noch keine Ereignisse",
|
"auditNoEvents": "Noch keine Ereignisse",
|
||||||
"auditNoEventsHint": "Starte einen Flow, um den Audit-Stream zu füllen.\nEreignisse erscheinen innerhalb von Sekunden.",
|
"auditNoEventsHint": "Starte einen Flow, um den Audit-Stream zu füllen.\nEreignisse erscheinen innerhalb von Sekunden.",
|
||||||
|
"auditFilterAll": "alle",
|
||||||
|
"auditFilterFlow": "Flow",
|
||||||
|
"auditFilterStep": "Step",
|
||||||
|
"auditFilterModule": "Modul",
|
||||||
|
"auditClearLogTooltip": "Protokoll löschen (nur local/dev Kanal)",
|
||||||
|
"auditClearedToast": "{n} Ereignisse auf Kanal „{channel}\" gelöscht. Chain mit Marker neu geseedet.",
|
||||||
|
"@auditClearedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"n": { "type": "int" },
|
||||||
|
"channel": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditClearFailed": "Löschen fehlgeschlagen: {detail}",
|
||||||
|
"@auditClearFailed": { "placeholders": { "detail": { "type": "String" } } },
|
||||||
|
"auditExplain": "Erklären",
|
||||||
|
"auditReask": "Neu fragen",
|
||||||
|
"auditAsking": "Frage läuft…",
|
||||||
|
"auditSystemAi": "SYSTEM-AI",
|
||||||
|
"auditCachedPill": "gecacht",
|
||||||
|
"auditOriginalLatency": "ursprünglich {ms} ms",
|
||||||
|
"@auditOriginalLatency": { "placeholders": { "ms": { "type": "int" } } },
|
||||||
|
"auditLatency": "{ms} ms",
|
||||||
|
"@auditLatency": { "placeholders": { "ms": { "type": "int" } } },
|
||||||
|
"auditRegenerateTooltip": "Neu generieren — Cache überspringen, Modell erneut fragen",
|
||||||
|
"auditLiveStatus": "live · Abfrage alle 2s · {n} {n, plural, =1{Ereignis} other{Ereignisse}}",
|
||||||
|
"@auditLiveStatus": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"auditDisconnected": "getrennt · {error}",
|
||||||
|
"@auditDisconnected": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"auditHashChainVerified": "Hash-Chain verifiziert",
|
||||||
|
"auditDetailHeader": "DETAIL",
|
||||||
|
"auditAskingFull": "Frage an die konfigurierte System-AI läuft…",
|
||||||
|
"auditFixLabel": "FIX",
|
||||||
|
"auditCachedTooltipKnown": "Aus dem lokalen Cache geliefert. Ursprünglich erzeugt am {at}{hits}.",
|
||||||
|
"@auditCachedTooltipKnown": {
|
||||||
|
"placeholders": {
|
||||||
|
"at": { "type": "String" },
|
||||||
|
"hits": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditCachedTooltipUnknown": "Aus dem lokalen Cache geliefert. Ursprünglicher Zeitpunkt unbekannt{hits}.",
|
||||||
|
"@auditCachedTooltipUnknown": { "placeholders": { "hits": { "type": "String" } } },
|
||||||
|
"auditCachedTooltipHits": " · {n} Treffer",
|
||||||
|
"@auditCachedTooltipHits": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"auditConfigureSystemAi": "System-AI unter Einstellungen konfigurieren (Cmd+,)",
|
||||||
|
"auditClearDialogTitle": "Audit-Log löschen?",
|
||||||
|
"auditClearDialogBody": "Löscht jedes Audit-Event auf dem aktiven Kanal und seedet einen neuen chain.reset-Marker mit Prüfer und Begründung. Auf beta / production verweigert. Nicht umkehrbar.",
|
||||||
|
"auditClearReviewerLabel": "Prüfer",
|
||||||
|
"auditClearReasonLabel": "Begründung (im chain.reset-Marker festgehalten)",
|
||||||
|
"auditClearReasonHelper": "Erforderlich — Auditoren werden das lesen.",
|
||||||
|
"auditClearLogButton": "Log löschen",
|
||||||
|
|
||||||
"modulesTitle": "Module",
|
"modulesTitle": "Module",
|
||||||
"modulesNoneTitle": "Noch keine Module",
|
"modulesNoneTitle": "Noch keine Module",
|
||||||
|
|
@ -103,6 +209,44 @@
|
||||||
"approvalsTabHistory": "Verlauf",
|
"approvalsTabHistory": "Verlauf",
|
||||||
"approvalsInboxZero": "Posteingang leer",
|
"approvalsInboxZero": "Posteingang leer",
|
||||||
"approvalsInboxHint": "Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.",
|
"approvalsInboxHint": "Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.",
|
||||||
|
"approvalsReloadTooltip": "Aktualisieren",
|
||||||
|
"approvalsPillPending": "offen",
|
||||||
|
"approvalsPillApproved": "freigegeben",
|
||||||
|
"approvalsPillRejected": "abgelehnt",
|
||||||
|
"approvalsPillExpired": "abgelaufen",
|
||||||
|
"approvalsPayloadPreview": "PAYLOAD-VORSCHAU",
|
||||||
|
"approvalsApproveButton": "Freigeben",
|
||||||
|
"approvalsRejectButton": "Ablehnen",
|
||||||
|
"approvalsRejectDialogTitle": "Freigabe ablehnen",
|
||||||
|
"approvalsRejectReasonLabel": "Begründung (im Audit-Log festgehalten)",
|
||||||
|
"approvalsApprovedToast": "freigegeben · {flow} › {step}",
|
||||||
|
"@approvalsApprovedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"flow": { "type": "String" },
|
||||||
|
"step": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvalsRejectedToast": "abgelehnt · {flow} › {step}",
|
||||||
|
"@approvalsRejectedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"flow": { "type": "String" },
|
||||||
|
"step": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvalsApproveFailed": "Freigabe fehlgeschlagen: {error}",
|
||||||
|
"@approvalsApproveFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"approvalsRejectFailed": "Ablehnung fehlgeschlagen: {error}",
|
||||||
|
"@approvalsRejectFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"approvalsHistoryEmpty": "Noch kein Verlauf",
|
||||||
|
"approvalsHistoryEmptyHint": "Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-chained Datensatz.",
|
||||||
|
"approvalsDialogDecided": "entschieden",
|
||||||
|
"approvalsDialogCreated": "erstellt",
|
||||||
|
"approvalsDialogReason": "Begründung",
|
||||||
|
"approvalsDialogPrompt": "PROMPT",
|
||||||
|
"approvalsExpiresIn": "{minutes}m",
|
||||||
|
"@approvalsExpiresIn": { "placeholders": { "minutes": { "type": "int" } } },
|
||||||
|
"approvalsExpiresInHours": "{hours}h",
|
||||||
|
"@approvalsExpiresInHours": { "placeholders": { "hours": { "type": "int" } } },
|
||||||
|
|
||||||
"doctorTitle": "Diagnose",
|
"doctorTitle": "Diagnose",
|
||||||
"doctorRecheckTooltip": "Erneut prüfen",
|
"doctorRecheckTooltip": "Erneut prüfen",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,62 @@
|
||||||
"settingsSaveAndConnect": "Save & connect",
|
"settingsSaveAndConnect": "Save & connect",
|
||||||
"settingsLanguage": "Language",
|
"settingsLanguage": "Language",
|
||||||
"settingsLanguageHint": "App language. Affects every page; bilingual store entries also flip via this toggle.",
|
"settingsLanguageHint": "App language. Affects every page; bilingual store entries also flip via this toggle.",
|
||||||
|
"settingsHubEndpointHint": "Where should Studio connect? Default is the local hub at 127.0.0.1:50051.",
|
||||||
|
"settingsTlsSubtitle": "https/grpc-secure",
|
||||||
|
"settingsPortError": "port must be 1–65535",
|
||||||
|
"channelsBlurb": "Switch hub channel (writes ~/.fai/current-channel and restarts the daemon). Connect just changes Studio's wire.",
|
||||||
|
"channelsActionsTooltip": "Channel actions",
|
||||||
|
"systemAiHeader": "SYSTEM AI",
|
||||||
|
"systemAiEnabled": "enabled",
|
||||||
|
"systemAiOff": "off",
|
||||||
|
"systemAiEdit": "Edit…",
|
||||||
|
"systemAiConfigure": "Configure…",
|
||||||
|
"systemAiOffBlurb": "Off — failure explanations on the audit page are disabled. Click Configure… to pick a provider (Ollama / OpenAI / LM Studio / vLLM / Custom). Operator config is rewritten in place; no daemon restart needed.",
|
||||||
|
"mcpServersCount": "{n} {n, plural, =1{server} other{servers}}",
|
||||||
|
"@mcpServersCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"mcpRediscoverTooltip": "Re-run discovery",
|
||||||
|
"mcpAddTooltip": "Add MCP server",
|
||||||
|
"mcpRemoveTooltip": "Remove server",
|
||||||
|
"mcpToolsCount": "{n} {n, plural, =1{tool} other{tools}}",
|
||||||
|
"@mcpToolsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nEndpointsCount": "{n} {n, plural, =1{endpoint} other{endpoints}}",
|
||||||
|
"@n8nEndpointsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nWorkflowsCount": "{n} {n, plural, =1{workflow} other{workflows}}",
|
||||||
|
"@n8nWorkflowsCount": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"n8nRediscoverTooltip": "Re-run discovery",
|
||||||
|
"n8nAddTooltip": "Add n8n endpoint",
|
||||||
|
"n8nRemoveTooltip": "Remove endpoint",
|
||||||
|
"addMcpTitle": "Add MCP server",
|
||||||
|
"addMcpIntro": "Tools the server advertises via `tools/list` appear in the Store as `mcp.<name>.<tool>` capabilities.",
|
||||||
|
"addMcpSuggestionsLabel": "Suggested servers — click to pre-fill",
|
||||||
|
"addMcpNameLabel": "Name (no dots)",
|
||||||
|
"addMcpNameHint": "filesystem",
|
||||||
|
"addMcpEndpointLabel": "Endpoint",
|
||||||
|
"addMcpEndpointHint": "http://127.0.0.1:3001/mcp",
|
||||||
|
"addMcpApiKeyLabel": "API key env var (optional)",
|
||||||
|
"addMcpApiKeyHint": "MCP_FILESYSTEM_TOKEN",
|
||||||
|
"addMcpNotesLabel": "Notes (optional)",
|
||||||
|
"addMcpAddButton": "Add + discover",
|
||||||
|
"addN8nTitle": "Add n8n endpoint",
|
||||||
|
"addN8nIntro": "Active workflows surface in the Store as `n8n.<name>.<workflow_slug>` capabilities.",
|
||||||
|
"addN8nNameLabel": "Name (no dots)",
|
||||||
|
"addN8nNameHint": "ops",
|
||||||
|
"addN8nBaseUrlLabel": "Base URL (without /api/v1)",
|
||||||
|
"addN8nBaseUrlHint": "https://n8n.example.com",
|
||||||
|
"addN8nApiKeyLabel": "API key env var (optional)",
|
||||||
|
"addN8nApiKeyHint": "N8N_OPS_KEY",
|
||||||
|
"addN8nNotesLabel": "Notes (optional)",
|
||||||
|
"addN8nAddButton": "Add + discover",
|
||||||
|
"addedMcpToast": "Added MCP server \"{name}\".",
|
||||||
|
"@addedMcpToast": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"addedN8nToast": "Added n8n endpoint \"{name}\".",
|
||||||
|
"@addedN8nToast": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"addFailedToast": "Add failed: {error}",
|
||||||
|
"@addFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"removeFailedToast": "Remove failed: {error}",
|
||||||
|
"@removeFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"refreshFailedToast": "Refresh failed: {error}",
|
||||||
|
"@refreshFailedToast": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
|
||||||
"channelsHeader": "CHANNELS",
|
"channelsHeader": "CHANNELS",
|
||||||
"channelsActive": "active",
|
"channelsActive": "active",
|
||||||
|
|
@ -69,6 +125,56 @@
|
||||||
"auditTitle": "Audit",
|
"auditTitle": "Audit",
|
||||||
"auditNoEvents": "No events yet",
|
"auditNoEvents": "No events yet",
|
||||||
"auditNoEventsHint": "Run a flow to populate the audit stream.\nEvents appear here within seconds.",
|
"auditNoEventsHint": "Run a flow to populate the audit stream.\nEvents appear here within seconds.",
|
||||||
|
"auditFilterAll": "all",
|
||||||
|
"auditFilterFlow": "flow",
|
||||||
|
"auditFilterStep": "step",
|
||||||
|
"auditFilterModule": "module",
|
||||||
|
"auditClearLogTooltip": "Clear log (local/dev channels only)",
|
||||||
|
"auditClearedToast": "Cleared {n} events on channel \"{channel}\". Chain reseeded with marker.",
|
||||||
|
"@auditClearedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"n": { "type": "int" },
|
||||||
|
"channel": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditClearFailed": "Clear failed: {detail}",
|
||||||
|
"@auditClearFailed": { "placeholders": { "detail": { "type": "String" } } },
|
||||||
|
"auditExplain": "Explain",
|
||||||
|
"auditReask": "Re-ask",
|
||||||
|
"auditAsking": "Asking…",
|
||||||
|
"auditSystemAi": "SYSTEM AI",
|
||||||
|
"auditCachedPill": "cached",
|
||||||
|
"auditOriginalLatency": "orig {ms} ms",
|
||||||
|
"@auditOriginalLatency": { "placeholders": { "ms": { "type": "int" } } },
|
||||||
|
"auditLatency": "{ms} ms",
|
||||||
|
"@auditLatency": { "placeholders": { "ms": { "type": "int" } } },
|
||||||
|
"auditRegenerateTooltip": "Regenerate — skip cache, ask the model again",
|
||||||
|
"auditLiveStatus": "live · polling 2s · {n} {n, plural, =1{event} other{events}}",
|
||||||
|
"@auditLiveStatus": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"auditDisconnected": "disconnected · {error}",
|
||||||
|
"@auditDisconnected": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"auditHashChainVerified": "hash chain verified",
|
||||||
|
"auditDetailHeader": "DETAIL",
|
||||||
|
"auditAskingFull": "Asking the configured System AI…",
|
||||||
|
"auditFixLabel": "FIX",
|
||||||
|
"auditCachedTooltipKnown": "Served from the local cache. Originally generated on {at}{hits}.",
|
||||||
|
"@auditCachedTooltipKnown": {
|
||||||
|
"placeholders": {
|
||||||
|
"at": { "type": "String" },
|
||||||
|
"hits": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditCachedTooltipUnknown": "Served from the local cache. Originally generated at unknown time{hits}.",
|
||||||
|
"@auditCachedTooltipUnknown": { "placeholders": { "hits": { "type": "String" } } },
|
||||||
|
"auditCachedTooltipHits": " · {n} hits",
|
||||||
|
"@auditCachedTooltipHits": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"auditConfigureSystemAi": "Configure System AI in Settings (Cmd+,)",
|
||||||
|
"auditClearDialogTitle": "Clear audit log?",
|
||||||
|
"auditClearDialogBody": "Wipes every audit event on the active channel and seeds a fresh chain.reset marker carrying reviewer + reason. Refused on beta / production. Irreversible.",
|
||||||
|
"auditClearReviewerLabel": "Reviewer",
|
||||||
|
"auditClearReasonLabel": "Reason (recorded in chain.reset marker)",
|
||||||
|
"auditClearReasonHelper": "Required — auditors will read this.",
|
||||||
|
"auditClearLogButton": "Clear log",
|
||||||
|
|
||||||
"modulesTitle": "Modules",
|
"modulesTitle": "Modules",
|
||||||
"modulesNoneTitle": "No modules yet",
|
"modulesNoneTitle": "No modules yet",
|
||||||
|
|
@ -104,6 +210,44 @@
|
||||||
"approvalsTabHistory": "History",
|
"approvalsTabHistory": "History",
|
||||||
"approvalsInboxZero": "Inbox zero",
|
"approvalsInboxZero": "Inbox zero",
|
||||||
"approvalsInboxHint": "All caught up. New `system.approval@^0` steps land here automatically.",
|
"approvalsInboxHint": "All caught up. New `system.approval@^0` steps land here automatically.",
|
||||||
|
"approvalsReloadTooltip": "Reload",
|
||||||
|
"approvalsPillPending": "pending",
|
||||||
|
"approvalsPillApproved": "approved",
|
||||||
|
"approvalsPillRejected": "rejected",
|
||||||
|
"approvalsPillExpired": "expired",
|
||||||
|
"approvalsPayloadPreview": "PAYLOAD PREVIEW",
|
||||||
|
"approvalsApproveButton": "Approve",
|
||||||
|
"approvalsRejectButton": "Reject",
|
||||||
|
"approvalsRejectDialogTitle": "Reject approval",
|
||||||
|
"approvalsRejectReasonLabel": "Reason (recorded in audit log)",
|
||||||
|
"approvalsApprovedToast": "approved · {flow} › {step}",
|
||||||
|
"@approvalsApprovedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"flow": { "type": "String" },
|
||||||
|
"step": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvalsRejectedToast": "rejected · {flow} › {step}",
|
||||||
|
"@approvalsRejectedToast": {
|
||||||
|
"placeholders": {
|
||||||
|
"flow": { "type": "String" },
|
||||||
|
"step": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvalsApproveFailed": "approve failed: {error}",
|
||||||
|
"@approvalsApproveFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"approvalsRejectFailed": "reject failed: {error}",
|
||||||
|
"@approvalsRejectFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"approvalsHistoryEmpty": "No history yet",
|
||||||
|
"approvalsHistoryEmptyHint": "Decided approvals (approved / rejected / expired) appear here.\nThe audit log keeps the full hash-chained record.",
|
||||||
|
"approvalsDialogDecided": "decided",
|
||||||
|
"approvalsDialogCreated": "created",
|
||||||
|
"approvalsDialogReason": "reason",
|
||||||
|
"approvalsDialogPrompt": "PROMPT",
|
||||||
|
"approvalsExpiresIn": "{minutes}m",
|
||||||
|
"@approvalsExpiresIn": { "placeholders": { "minutes": { "type": "int" } } },
|
||||||
|
"approvalsExpiresInHours": "{hours}h",
|
||||||
|
"@approvalsExpiresInHours": { "placeholders": { "hours": { "type": "int" } } },
|
||||||
|
|
||||||
"doctorTitle": "Doctor",
|
"doctorTitle": "Doctor",
|
||||||
"doctorRecheckTooltip": "Re-check",
|
"doctorRecheckTooltip": "Re-check",
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,288 @@ abstract class AppLocalizations {
|
||||||
/// **'App language. Affects every page; bilingual store entries also flip via this toggle.'**
|
/// **'App language. Affects every page; bilingual store entries also flip via this toggle.'**
|
||||||
String get settingsLanguageHint;
|
String get settingsLanguageHint;
|
||||||
|
|
||||||
|
/// No description provided for @settingsHubEndpointHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Where should Studio connect? Default is the local hub at 127.0.0.1:50051.'**
|
||||||
|
String get settingsHubEndpointHint;
|
||||||
|
|
||||||
|
/// No description provided for @settingsTlsSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'https/grpc-secure'**
|
||||||
|
String get settingsTlsSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settingsPortError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'port must be 1–65535'**
|
||||||
|
String get settingsPortError;
|
||||||
|
|
||||||
|
/// No description provided for @channelsBlurb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Switch hub channel (writes ~/.fai/current-channel and restarts the daemon). Connect just changes Studio\'s wire.'**
|
||||||
|
String get channelsBlurb;
|
||||||
|
|
||||||
|
/// No description provided for @channelsActionsTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel actions'**
|
||||||
|
String get channelsActionsTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiHeader.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'SYSTEM AI'**
|
||||||
|
String get systemAiHeader;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiEnabled.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'enabled'**
|
||||||
|
String get systemAiEnabled;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiOff.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'off'**
|
||||||
|
String get systemAiOff;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiEdit.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Edit…'**
|
||||||
|
String get systemAiEdit;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiConfigure.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Configure…'**
|
||||||
|
String get systemAiConfigure;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiOffBlurb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Off — failure explanations on the audit page are disabled. Click Configure… to pick a provider (Ollama / OpenAI / LM Studio / vLLM / Custom). Operator config is rewritten in place; no daemon restart needed.'**
|
||||||
|
String get systemAiOffBlurb;
|
||||||
|
|
||||||
|
/// No description provided for @mcpServersCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{n} {n, plural, =1{server} other{servers}}'**
|
||||||
|
String mcpServersCount(int n);
|
||||||
|
|
||||||
|
/// No description provided for @mcpRediscoverTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Re-run discovery'**
|
||||||
|
String get mcpRediscoverTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @mcpAddTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add MCP server'**
|
||||||
|
String get mcpAddTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @mcpRemoveTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove server'**
|
||||||
|
String get mcpRemoveTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @mcpToolsCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{n} {n, plural, =1{tool} other{tools}}'**
|
||||||
|
String mcpToolsCount(int n);
|
||||||
|
|
||||||
|
/// No description provided for @n8nEndpointsCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{n} {n, plural, =1{endpoint} other{endpoints}}'**
|
||||||
|
String n8nEndpointsCount(int n);
|
||||||
|
|
||||||
|
/// No description provided for @n8nWorkflowsCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{n} {n, plural, =1{workflow} other{workflows}}'**
|
||||||
|
String n8nWorkflowsCount(int n);
|
||||||
|
|
||||||
|
/// No description provided for @n8nRediscoverTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Re-run discovery'**
|
||||||
|
String get n8nRediscoverTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @n8nAddTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add n8n endpoint'**
|
||||||
|
String get n8nAddTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @n8nRemoveTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove endpoint'**
|
||||||
|
String get n8nRemoveTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add MCP server'**
|
||||||
|
String get addMcpTitle;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tools the server advertises via `tools/list` appear in the Store as `mcp.<name>.<tool>` capabilities.'**
|
||||||
|
String get addMcpIntro;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpSuggestionsLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Suggested servers — click to pre-fill'**
|
||||||
|
String get addMcpSuggestionsLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpNameLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Name (no dots)'**
|
||||||
|
String get addMcpNameLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpNameHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'filesystem'**
|
||||||
|
String get addMcpNameHint;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpEndpointLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Endpoint'**
|
||||||
|
String get addMcpEndpointLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpEndpointHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'http://127.0.0.1:3001/mcp'**
|
||||||
|
String get addMcpEndpointHint;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpApiKeyLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API key env var (optional)'**
|
||||||
|
String get addMcpApiKeyLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpApiKeyHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'MCP_FILESYSTEM_TOKEN'**
|
||||||
|
String get addMcpApiKeyHint;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpNotesLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Notes (optional)'**
|
||||||
|
String get addMcpNotesLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addMcpAddButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add + discover'**
|
||||||
|
String get addMcpAddButton;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add n8n endpoint'**
|
||||||
|
String get addN8nTitle;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Active workflows surface in the Store as `n8n.<name>.<workflow_slug>` capabilities.'**
|
||||||
|
String get addN8nIntro;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nNameLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Name (no dots)'**
|
||||||
|
String get addN8nNameLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nNameHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'ops'**
|
||||||
|
String get addN8nNameHint;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nBaseUrlLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Base URL (without /api/v1)'**
|
||||||
|
String get addN8nBaseUrlLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nBaseUrlHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'https://n8n.example.com'**
|
||||||
|
String get addN8nBaseUrlHint;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nApiKeyLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API key env var (optional)'**
|
||||||
|
String get addN8nApiKeyLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nApiKeyHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'N8N_OPS_KEY'**
|
||||||
|
String get addN8nApiKeyHint;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nNotesLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Notes (optional)'**
|
||||||
|
String get addN8nNotesLabel;
|
||||||
|
|
||||||
|
/// No description provided for @addN8nAddButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add + discover'**
|
||||||
|
String get addN8nAddButton;
|
||||||
|
|
||||||
|
/// No description provided for @addedMcpToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Added MCP server \"{name}\".'**
|
||||||
|
String addedMcpToast(String name);
|
||||||
|
|
||||||
|
/// No description provided for @addedN8nToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Added n8n endpoint \"{name}\".'**
|
||||||
|
String addedN8nToast(String name);
|
||||||
|
|
||||||
|
/// No description provided for @addFailedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add failed: {error}'**
|
||||||
|
String addFailedToast(String error);
|
||||||
|
|
||||||
|
/// No description provided for @removeFailedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove failed: {error}'**
|
||||||
|
String removeFailedToast(String error);
|
||||||
|
|
||||||
|
/// No description provided for @refreshFailedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh failed: {error}'**
|
||||||
|
String refreshFailedToast(String error);
|
||||||
|
|
||||||
/// No description provided for @channelsHeader.
|
/// No description provided for @channelsHeader.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -440,6 +722,192 @@ abstract class AppLocalizations {
|
||||||
/// **'Run a flow to populate the audit stream.\nEvents appear here within seconds.'**
|
/// **'Run a flow to populate the audit stream.\nEvents appear here within seconds.'**
|
||||||
String get auditNoEventsHint;
|
String get auditNoEventsHint;
|
||||||
|
|
||||||
|
/// No description provided for @auditFilterAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'all'**
|
||||||
|
String get auditFilterAll;
|
||||||
|
|
||||||
|
/// No description provided for @auditFilterFlow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'flow'**
|
||||||
|
String get auditFilterFlow;
|
||||||
|
|
||||||
|
/// No description provided for @auditFilterStep.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'step'**
|
||||||
|
String get auditFilterStep;
|
||||||
|
|
||||||
|
/// No description provided for @auditFilterModule.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'module'**
|
||||||
|
String get auditFilterModule;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearLogTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear log (local/dev channels only)'**
|
||||||
|
String get auditClearLogTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cleared {n} events on channel \"{channel}\". Chain reseeded with marker.'**
|
||||||
|
String auditClearedToast(int n, String channel);
|
||||||
|
|
||||||
|
/// No description provided for @auditClearFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear failed: {detail}'**
|
||||||
|
String auditClearFailed(String detail);
|
||||||
|
|
||||||
|
/// No description provided for @auditExplain.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Explain'**
|
||||||
|
String get auditExplain;
|
||||||
|
|
||||||
|
/// No description provided for @auditReask.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Re-ask'**
|
||||||
|
String get auditReask;
|
||||||
|
|
||||||
|
/// No description provided for @auditAsking.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Asking…'**
|
||||||
|
String get auditAsking;
|
||||||
|
|
||||||
|
/// No description provided for @auditSystemAi.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'SYSTEM AI'**
|
||||||
|
String get auditSystemAi;
|
||||||
|
|
||||||
|
/// No description provided for @auditCachedPill.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'cached'**
|
||||||
|
String get auditCachedPill;
|
||||||
|
|
||||||
|
/// No description provided for @auditOriginalLatency.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'orig {ms} ms'**
|
||||||
|
String auditOriginalLatency(int ms);
|
||||||
|
|
||||||
|
/// No description provided for @auditLatency.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{ms} ms'**
|
||||||
|
String auditLatency(int ms);
|
||||||
|
|
||||||
|
/// No description provided for @auditRegenerateTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Regenerate — skip cache, ask the model again'**
|
||||||
|
String get auditRegenerateTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @auditLiveStatus.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'live · polling 2s · {n} {n, plural, =1{event} other{events}}'**
|
||||||
|
String auditLiveStatus(int n);
|
||||||
|
|
||||||
|
/// No description provided for @auditDisconnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'disconnected · {error}'**
|
||||||
|
String auditDisconnected(String error);
|
||||||
|
|
||||||
|
/// No description provided for @auditHashChainVerified.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'hash chain verified'**
|
||||||
|
String get auditHashChainVerified;
|
||||||
|
|
||||||
|
/// No description provided for @auditDetailHeader.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'DETAIL'**
|
||||||
|
String get auditDetailHeader;
|
||||||
|
|
||||||
|
/// No description provided for @auditAskingFull.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Asking the configured System AI…'**
|
||||||
|
String get auditAskingFull;
|
||||||
|
|
||||||
|
/// No description provided for @auditFixLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'FIX'**
|
||||||
|
String get auditFixLabel;
|
||||||
|
|
||||||
|
/// No description provided for @auditCachedTooltipKnown.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Served from the local cache. Originally generated on {at}{hits}.'**
|
||||||
|
String auditCachedTooltipKnown(String at, String hits);
|
||||||
|
|
||||||
|
/// No description provided for @auditCachedTooltipUnknown.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Served from the local cache. Originally generated at unknown time{hits}.'**
|
||||||
|
String auditCachedTooltipUnknown(String hits);
|
||||||
|
|
||||||
|
/// No description provided for @auditCachedTooltipHits.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **' · {n} hits'**
|
||||||
|
String auditCachedTooltipHits(int n);
|
||||||
|
|
||||||
|
/// No description provided for @auditConfigureSystemAi.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Configure System AI in Settings (Cmd+,)'**
|
||||||
|
String get auditConfigureSystemAi;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearDialogTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear audit log?'**
|
||||||
|
String get auditClearDialogTitle;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearDialogBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Wipes every audit event on the active channel and seeds a fresh chain.reset marker carrying reviewer + reason. Refused on beta / production. Irreversible.'**
|
||||||
|
String get auditClearDialogBody;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearReviewerLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reviewer'**
|
||||||
|
String get auditClearReviewerLabel;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearReasonLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reason (recorded in chain.reset marker)'**
|
||||||
|
String get auditClearReasonLabel;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearReasonHelper.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Required — auditors will read this.'**
|
||||||
|
String get auditClearReasonHelper;
|
||||||
|
|
||||||
|
/// No description provided for @auditClearLogButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear log'**
|
||||||
|
String get auditClearLogButton;
|
||||||
|
|
||||||
/// No description provided for @modulesTitle.
|
/// No description provided for @modulesTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -554,6 +1022,138 @@ abstract class AppLocalizations {
|
||||||
/// **'All caught up. New `system.approval@^0` steps land here automatically.'**
|
/// **'All caught up. New `system.approval@^0` steps land here automatically.'**
|
||||||
String get approvalsInboxHint;
|
String get approvalsInboxHint;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsReloadTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reload'**
|
||||||
|
String get approvalsReloadTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsPillPending.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'pending'**
|
||||||
|
String get approvalsPillPending;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsPillApproved.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'approved'**
|
||||||
|
String get approvalsPillApproved;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsPillRejected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'rejected'**
|
||||||
|
String get approvalsPillRejected;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsPillExpired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'expired'**
|
||||||
|
String get approvalsPillExpired;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsPayloadPreview.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'PAYLOAD PREVIEW'**
|
||||||
|
String get approvalsPayloadPreview;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsApproveButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Approve'**
|
||||||
|
String get approvalsApproveButton;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsRejectButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reject'**
|
||||||
|
String get approvalsRejectButton;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsRejectDialogTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reject approval'**
|
||||||
|
String get approvalsRejectDialogTitle;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsRejectReasonLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reason (recorded in audit log)'**
|
||||||
|
String get approvalsRejectReasonLabel;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsApprovedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'approved · {flow} › {step}'**
|
||||||
|
String approvalsApprovedToast(String flow, String step);
|
||||||
|
|
||||||
|
/// No description provided for @approvalsRejectedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'rejected · {flow} › {step}'**
|
||||||
|
String approvalsRejectedToast(String flow, String step);
|
||||||
|
|
||||||
|
/// No description provided for @approvalsApproveFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'approve failed: {error}'**
|
||||||
|
String approvalsApproveFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @approvalsRejectFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'reject failed: {error}'**
|
||||||
|
String approvalsRejectFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @approvalsHistoryEmpty.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No history yet'**
|
||||||
|
String get approvalsHistoryEmpty;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsHistoryEmptyHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Decided approvals (approved / rejected / expired) appear here.\nThe audit log keeps the full hash-chained record.'**
|
||||||
|
String get approvalsHistoryEmptyHint;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsDialogDecided.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'decided'**
|
||||||
|
String get approvalsDialogDecided;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsDialogCreated.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'created'**
|
||||||
|
String get approvalsDialogCreated;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsDialogReason.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'reason'**
|
||||||
|
String get approvalsDialogReason;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsDialogPrompt.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'PROMPT'**
|
||||||
|
String get approvalsDialogPrompt;
|
||||||
|
|
||||||
|
/// No description provided for @approvalsExpiresIn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{minutes}m'**
|
||||||
|
String approvalsExpiresIn(int minutes);
|
||||||
|
|
||||||
|
/// No description provided for @approvalsExpiresInHours.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{hours}h'**
|
||||||
|
String approvalsExpiresInHours(int hours);
|
||||||
|
|
||||||
/// No description provided for @doctorTitle.
|
/// No description provided for @doctorTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,182 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get settingsLanguageHint =>
|
String get settingsLanguageHint =>
|
||||||
'Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.';
|
'Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsHubEndpointHint =>
|
||||||
|
'Wohin soll Studio sich verbinden? Standard ist der lokale Hub auf 127.0.0.1:50051.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsTlsSubtitle => 'https/grpc-secure';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPortError => 'Port muss 1–65535 sein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelsBlurb =>
|
||||||
|
'Hub-Kanal wechseln (schreibt ~/.fai/current-channel und startet den Daemon neu). Verbinden ändert nur die Wire von Studio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelsActionsTooltip => 'Kanal-Aktionen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiHeader => 'SYSTEM-AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEnabled => 'aktiv';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiOff => 'aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEdit => 'Bearbeiten…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConfigure => 'Konfigurieren…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiOffBlurb =>
|
||||||
|
'Aus — Fehlererklärungen auf der Protokoll-Seite sind deaktiviert. Klicke Konfigurieren…, um einen Provider zu wählen (Ollama / OpenAI / LM Studio / vLLM / Eigene). Die Operator-Konfig wird direkt überschrieben; kein Daemon-Neustart nötig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mcpServersCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Server',
|
||||||
|
one: 'Server',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpRediscoverTooltip => 'Discovery neu ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpAddTooltip => 'MCP-Server hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpRemoveTooltip => 'Server entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mcpToolsCount(int n) {
|
||||||
|
return '$n Tools';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String n8nEndpointsCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Endpunkte',
|
||||||
|
one: 'Endpunkt',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String n8nWorkflowsCount(int n) {
|
||||||
|
return '$n Workflows';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nRediscoverTooltip => 'Discovery neu ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nAddTooltip => 'n8n-Endpunkt hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nRemoveTooltip => 'Endpunkt entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpTitle => 'MCP-Server hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpIntro =>
|
||||||
|
'Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Capabilities.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpSuggestionsLabel => 'Vorschläge — zum Vorausfüllen klicken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNameLabel => 'Name (keine Punkte)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNameHint => 'filesystem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpEndpointLabel => 'Endpunkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpEndpointHint => 'http://127.0.0.1:3001/mcp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpApiKeyLabel => 'API-Key-Umgebungsvariable (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpApiKeyHint => 'MCP_FILESYSTEM_TOKEN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNotesLabel => 'Notizen (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpAddButton => 'Hinzufügen + Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nTitle => 'n8n-Endpunkt hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nIntro =>
|
||||||
|
'Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Capabilities.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNameLabel => 'Name (keine Punkte)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNameHint => 'ops';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nBaseUrlLabel => 'Basis-URL (ohne /api/v1)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nBaseUrlHint => 'https://n8n.example.com';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nApiKeyLabel => 'API-Key-Umgebungsvariable (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nApiKeyHint => 'N8N_OPS_KEY';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNotesLabel => 'Notizen (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nAddButton => 'Hinzufügen + Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addedMcpToast(String name) {
|
||||||
|
return 'MCP-Server „$name\" hinzugefügt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addedN8nToast(String name) {
|
||||||
|
return 'n8n-Endpunkt „$name\" hinzugefügt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addFailedToast(String error) {
|
||||||
|
return 'Hinzufügen fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String removeFailedToast(String error) {
|
||||||
|
return 'Entfernen fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String refreshFailedToast(String error) {
|
||||||
|
return 'Aktualisieren fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelsHeader => 'KANÄLE';
|
String get channelsHeader => 'KANÄLE';
|
||||||
|
|
||||||
|
|
@ -186,6 +362,128 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get auditNoEventsHint =>
|
String get auditNoEventsHint =>
|
||||||
'Starte einen Flow, um den Audit-Stream zu füllen.\nEreignisse erscheinen innerhalb von Sekunden.';
|
'Starte einen Flow, um den Audit-Stream zu füllen.\nEreignisse erscheinen innerhalb von Sekunden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterAll => 'alle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterFlow => 'Flow';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterStep => 'Step';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterModule => 'Modul';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearLogTooltip => 'Protokoll löschen (nur local/dev Kanal)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditClearedToast(int n, String channel) {
|
||||||
|
return '$n Ereignisse auf Kanal „$channel\" gelöscht. Chain mit Marker neu geseedet.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditClearFailed(String detail) {
|
||||||
|
return 'Löschen fehlgeschlagen: $detail';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditExplain => 'Erklären';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditReask => 'Neu fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditAsking => 'Frage läuft…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditSystemAi => 'SYSTEM-AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditCachedPill => 'gecacht';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditOriginalLatency(int ms) {
|
||||||
|
return 'ursprünglich $ms ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditLatency(int ms) {
|
||||||
|
return '$ms ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditRegenerateTooltip =>
|
||||||
|
'Neu generieren — Cache überspringen, Modell erneut fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditLiveStatus(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Ereignisse',
|
||||||
|
one: 'Ereignis',
|
||||||
|
);
|
||||||
|
return 'live · Abfrage alle 2s · $n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditDisconnected(String error) {
|
||||||
|
return 'getrennt · $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditHashChainVerified => 'Hash-Chain verifiziert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditDetailHeader => 'DETAIL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditAskingFull => 'Frage an die konfigurierte System-AI läuft…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFixLabel => 'FIX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipKnown(String at, String hits) {
|
||||||
|
return 'Aus dem lokalen Cache geliefert. Ursprünglich erzeugt am $at$hits.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipUnknown(String hits) {
|
||||||
|
return 'Aus dem lokalen Cache geliefert. Ursprünglicher Zeitpunkt unbekannt$hits.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipHits(int n) {
|
||||||
|
return ' · $n Treffer';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditConfigureSystemAi =>
|
||||||
|
'System-AI unter Einstellungen konfigurieren (Cmd+,)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearDialogTitle => 'Audit-Log löschen?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearDialogBody =>
|
||||||
|
'Löscht jedes Audit-Event auf dem aktiven Kanal und seedet einen neuen chain.reset-Marker mit Prüfer und Begründung. Auf beta / production verweigert. Nicht umkehrbar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReviewerLabel => 'Prüfer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReasonLabel =>
|
||||||
|
'Begründung (im chain.reset-Marker festgehalten)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReasonHelper =>
|
||||||
|
'Erforderlich — Auditoren werden das lesen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearLogButton => 'Log löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get modulesTitle => 'Module';
|
String get modulesTitle => 'Module';
|
||||||
|
|
||||||
|
|
@ -253,6 +551,86 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get approvalsInboxHint =>
|
String get approvalsInboxHint =>
|
||||||
'Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.';
|
'Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsReloadTooltip => 'Aktualisieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillPending => 'offen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillApproved => 'freigegeben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillRejected => 'abgelehnt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillExpired => 'abgelaufen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPayloadPreview => 'PAYLOAD-VORSCHAU';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsApproveButton => 'Freigeben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectButton => 'Ablehnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectDialogTitle => 'Freigabe ablehnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectReasonLabel =>
|
||||||
|
'Begründung (im Audit-Log festgehalten)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsApprovedToast(String flow, String step) {
|
||||||
|
return 'freigegeben · $flow › $step';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsRejectedToast(String flow, String step) {
|
||||||
|
return 'abgelehnt · $flow › $step';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsApproveFailed(String error) {
|
||||||
|
return 'Freigabe fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsRejectFailed(String error) {
|
||||||
|
return 'Ablehnung fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsHistoryEmpty => 'Noch kein Verlauf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsHistoryEmptyHint =>
|
||||||
|
'Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-chained Datensatz.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogDecided => 'entschieden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogCreated => 'erstellt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogReason => 'Begründung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogPrompt => 'PROMPT';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsExpiresIn(int minutes) {
|
||||||
|
return '${minutes}m';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsExpiresInHours(int hours) {
|
||||||
|
return '${hours}h';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorTitle => 'Diagnose';
|
String get doctorTitle => 'Diagnose';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,194 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsLanguageHint =>
|
String get settingsLanguageHint =>
|
||||||
'App language. Affects every page; bilingual store entries also flip via this toggle.';
|
'App language. Affects every page; bilingual store entries also flip via this toggle.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsHubEndpointHint =>
|
||||||
|
'Where should Studio connect? Default is the local hub at 127.0.0.1:50051.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsTlsSubtitle => 'https/grpc-secure';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPortError => 'port must be 1–65535';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelsBlurb =>
|
||||||
|
'Switch hub channel (writes ~/.fai/current-channel and restarts the daemon). Connect just changes Studio\'s wire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelsActionsTooltip => 'Channel actions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiHeader => 'SYSTEM AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEnabled => 'enabled';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiOff => 'off';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEdit => 'Edit…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConfigure => 'Configure…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiOffBlurb =>
|
||||||
|
'Off — failure explanations on the audit page are disabled. Click Configure… to pick a provider (Ollama / OpenAI / LM Studio / vLLM / Custom). Operator config is rewritten in place; no daemon restart needed.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mcpServersCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'servers',
|
||||||
|
one: 'server',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpRediscoverTooltip => 'Re-run discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpAddTooltip => 'Add MCP server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mcpRemoveTooltip => 'Remove server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mcpToolsCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'tools',
|
||||||
|
one: 'tool',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String n8nEndpointsCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'endpoints',
|
||||||
|
one: 'endpoint',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String n8nWorkflowsCount(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'workflows',
|
||||||
|
one: 'workflow',
|
||||||
|
);
|
||||||
|
return '$n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nRediscoverTooltip => 'Re-run discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nAddTooltip => 'Add n8n endpoint';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get n8nRemoveTooltip => 'Remove endpoint';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpTitle => 'Add MCP server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpIntro =>
|
||||||
|
'Tools the server advertises via `tools/list` appear in the Store as `mcp.<name>.<tool>` capabilities.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpSuggestionsLabel => 'Suggested servers — click to pre-fill';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNameLabel => 'Name (no dots)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNameHint => 'filesystem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpEndpointLabel => 'Endpoint';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpEndpointHint => 'http://127.0.0.1:3001/mcp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpApiKeyLabel => 'API key env var (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpApiKeyHint => 'MCP_FILESYSTEM_TOKEN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpNotesLabel => 'Notes (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addMcpAddButton => 'Add + discover';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nTitle => 'Add n8n endpoint';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nIntro =>
|
||||||
|
'Active workflows surface in the Store as `n8n.<name>.<workflow_slug>` capabilities.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNameLabel => 'Name (no dots)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNameHint => 'ops';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nBaseUrlLabel => 'Base URL (without /api/v1)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nBaseUrlHint => 'https://n8n.example.com';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nApiKeyLabel => 'API key env var (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nApiKeyHint => 'N8N_OPS_KEY';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nNotesLabel => 'Notes (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addN8nAddButton => 'Add + discover';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addedMcpToast(String name) {
|
||||||
|
return 'Added MCP server \"$name\".';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addedN8nToast(String name) {
|
||||||
|
return 'Added n8n endpoint \"$name\".';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addFailedToast(String error) {
|
||||||
|
return 'Add failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String removeFailedToast(String error) {
|
||||||
|
return 'Remove failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String refreshFailedToast(String error) {
|
||||||
|
return 'Refresh failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelsHeader => 'CHANNELS';
|
String get channelsHeader => 'CHANNELS';
|
||||||
|
|
||||||
|
|
@ -192,6 +380,126 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get auditNoEventsHint =>
|
String get auditNoEventsHint =>
|
||||||
'Run a flow to populate the audit stream.\nEvents appear here within seconds.';
|
'Run a flow to populate the audit stream.\nEvents appear here within seconds.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterAll => 'all';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterFlow => 'flow';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterStep => 'step';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFilterModule => 'module';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearLogTooltip => 'Clear log (local/dev channels only)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditClearedToast(int n, String channel) {
|
||||||
|
return 'Cleared $n events on channel \"$channel\". Chain reseeded with marker.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditClearFailed(String detail) {
|
||||||
|
return 'Clear failed: $detail';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditExplain => 'Explain';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditReask => 'Re-ask';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditAsking => 'Asking…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditSystemAi => 'SYSTEM AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditCachedPill => 'cached';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditOriginalLatency(int ms) {
|
||||||
|
return 'orig $ms ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditLatency(int ms) {
|
||||||
|
return '$ms ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditRegenerateTooltip =>
|
||||||
|
'Regenerate — skip cache, ask the model again';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditLiveStatus(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'events',
|
||||||
|
one: 'event',
|
||||||
|
);
|
||||||
|
return 'live · polling 2s · $n $_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditDisconnected(String error) {
|
||||||
|
return 'disconnected · $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditHashChainVerified => 'hash chain verified';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditDetailHeader => 'DETAIL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditAskingFull => 'Asking the configured System AI…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditFixLabel => 'FIX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipKnown(String at, String hits) {
|
||||||
|
return 'Served from the local cache. Originally generated on $at$hits.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipUnknown(String hits) {
|
||||||
|
return 'Served from the local cache. Originally generated at unknown time$hits.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String auditCachedTooltipHits(int n) {
|
||||||
|
return ' · $n hits';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditConfigureSystemAi =>
|
||||||
|
'Configure System AI in Settings (Cmd+,)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearDialogTitle => 'Clear audit log?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearDialogBody =>
|
||||||
|
'Wipes every audit event on the active channel and seeds a fresh chain.reset marker carrying reviewer + reason. Refused on beta / production. Irreversible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReviewerLabel => 'Reviewer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReasonLabel => 'Reason (recorded in chain.reset marker)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearReasonHelper => 'Required — auditors will read this.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get auditClearLogButton => 'Clear log';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get modulesTitle => 'Modules';
|
String get modulesTitle => 'Modules';
|
||||||
|
|
||||||
|
|
@ -259,6 +567,85 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get approvalsInboxHint =>
|
String get approvalsInboxHint =>
|
||||||
'All caught up. New `system.approval@^0` steps land here automatically.';
|
'All caught up. New `system.approval@^0` steps land here automatically.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsReloadTooltip => 'Reload';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillPending => 'pending';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillApproved => 'approved';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillRejected => 'rejected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPillExpired => 'expired';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsPayloadPreview => 'PAYLOAD PREVIEW';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsApproveButton => 'Approve';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectButton => 'Reject';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectDialogTitle => 'Reject approval';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsRejectReasonLabel => 'Reason (recorded in audit log)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsApprovedToast(String flow, String step) {
|
||||||
|
return 'approved · $flow › $step';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsRejectedToast(String flow, String step) {
|
||||||
|
return 'rejected · $flow › $step';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsApproveFailed(String error) {
|
||||||
|
return 'approve failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsRejectFailed(String error) {
|
||||||
|
return 'reject failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsHistoryEmpty => 'No history yet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsHistoryEmptyHint =>
|
||||||
|
'Decided approvals (approved / rejected / expired) appear here.\nThe audit log keeps the full hash-chained record.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogDecided => 'decided';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogCreated => 'created';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogReason => 'reason';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get approvalsDialogPrompt => 'PROMPT';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsExpiresIn(int minutes) {
|
||||||
|
return '${minutes}m';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String approvalsExpiresInHours(int hours) {
|
||||||
|
return '${hours}h';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorTitle => 'Doctor';
|
String get doctorTitle => 'Doctor';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,24 +59,26 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _approve(ApprovalRecord a) async {
|
Future<void> _approve(ApprovalRecord a) async {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
try {
|
try {
|
||||||
await HubService.instance.approve(a.id, _reviewer);
|
await HubService.instance.approve(a.id, _reviewer);
|
||||||
_toast('approved · ${a.flowName} › ${a.stepId}');
|
_toast(l.approvalsApprovedToast(a.flowName, a.stepId));
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_toast('approve failed: $e');
|
_toast(l.approvalsApproveFailed(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reject(ApprovalRecord a) async {
|
Future<void> _reject(ApprovalRecord a) async {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final reason = await _promptReason(context);
|
final reason = await _promptReason(context);
|
||||||
if (reason == null || reason.isEmpty) return;
|
if (reason == null || reason.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
await HubService.instance.reject(a.id, _reviewer, reason);
|
await HubService.instance.reject(a.id, _reviewer, reason);
|
||||||
_toast('rejected · ${a.flowName} › ${a.stepId}');
|
_toast(l.approvalsRejectedToast(a.flowName, a.stepId));
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_toast('reject failed: $e');
|
_toast(l.approvalsRejectFailed(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,22 +89,23 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
|
|
||||||
Future<String?> _promptReason(BuildContext context) async {
|
Future<String?> _promptReason(BuildContext context) async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return showDialog<String>(
|
return showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Reject approval'),
|
title: Text(l.approvalsRejectDialogTitle),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Reason (recorded in audit log)',
|
labelText: l.approvalsRejectReasonLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, null),
|
onPressed: () => Navigator.pop(ctx, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(ctx, controller.text),
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||||
|
|
@ -110,7 +113,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||||
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||||||
),
|
),
|
||||||
child: const Text('Reject'),
|
child: Text(l.approvalsRejectButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -134,7 +137,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 18),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
tooltip: 'Reload',
|
tooltip: AppLocalizations.of(context)!.approvalsReloadTooltip,
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
|
@ -175,6 +178,7 @@ class _PendingList extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return FutureBuilder<List<ApprovalRecord>>(
|
return FutureBuilder<List<ApprovalRecord>>(
|
||||||
future: future,
|
future: future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -185,21 +189,20 @@ class _PendingList extends StatelessWidget {
|
||||||
return FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.cloud_off_outlined,
|
icon: Icons.cloud_off_outlined,
|
||||||
iconColor: theme.colorScheme.error,
|
iconColor: theme.colorScheme.error,
|
||||||
title: 'Hub unreachable',
|
title: l.hubUnreachable,
|
||||||
hint: 'Start the hub with `fai serve`.',
|
hint: l.hubUnreachableHint,
|
||||||
action: FilledButton.tonal(
|
action: FilledButton.tonal(
|
||||||
onPressed: onRetry,
|
onPressed: onRetry,
|
||||||
child: const Text('Retry'),
|
child: Text(l.buttonRetry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final pending = snapshot.data ?? [];
|
final pending = snapshot.data ?? [];
|
||||||
if (pending.isEmpty) {
|
if (pending.isEmpty) {
|
||||||
return const FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.task_alt_outlined,
|
icon: Icons.task_alt_outlined,
|
||||||
title: 'Inbox zero',
|
title: l.approvalsInboxZero,
|
||||||
hint:
|
hint: l.approvalsInboxHint,
|
||||||
'All caught up. New `system.approval@^0` steps land here automatically.',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
|
@ -229,6 +232,7 @@ class _HistoryList extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return FutureBuilder<List<ApprovalRecord>>(
|
return FutureBuilder<List<ApprovalRecord>>(
|
||||||
future: future,
|
future: future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -239,21 +243,20 @@ class _HistoryList extends StatelessWidget {
|
||||||
return FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.cloud_off_outlined,
|
icon: Icons.cloud_off_outlined,
|
||||||
iconColor: theme.colorScheme.error,
|
iconColor: theme.colorScheme.error,
|
||||||
title: 'Hub unreachable',
|
title: l.hubUnreachable,
|
||||||
hint: 'Start the hub with `fai serve`.',
|
hint: l.hubUnreachableHint,
|
||||||
action: FilledButton.tonal(
|
action: FilledButton.tonal(
|
||||||
onPressed: onRetry,
|
onPressed: onRetry,
|
||||||
child: const Text('Retry'),
|
child: Text(l.buttonRetry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final decided = snapshot.data ?? [];
|
final decided = snapshot.data ?? [];
|
||||||
if (decided.isEmpty) {
|
if (decided.isEmpty) {
|
||||||
return const FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.history,
|
icon: Icons.history,
|
||||||
title: 'No history yet',
|
title: l.approvalsHistoryEmpty,
|
||||||
hint:
|
hint: l.approvalsHistoryEmptyHint,
|
||||||
'Decided approvals (approved / rejected / expired) appear here.\nThe audit log keeps the full hash-chained record.',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
|
@ -284,6 +287,7 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return FaiCard(
|
return FaiCard(
|
||||||
accentTop: true,
|
accentTop: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -291,8 +295,8 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const FaiPill(
|
FaiPill(
|
||||||
label: 'pending',
|
label: l.approvalsPillPending,
|
||||||
tone: FaiPillTone.warning,
|
tone: FaiPillTone.warning,
|
||||||
icon: Icons.pending_outlined,
|
icon: Icons.pending_outlined,
|
||||||
),
|
),
|
||||||
|
|
@ -307,7 +311,7 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (approval.expiresAt != null)
|
if (approval.expiresAt != null)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: _expiresIn(approval.expiresAt!),
|
label: _expiresInLabel(context, approval.expiresAt!),
|
||||||
tone: FaiPillTone.neutral,
|
tone: FaiPillTone.neutral,
|
||||||
icon: Icons.schedule,
|
icon: Icons.schedule,
|
||||||
),
|
),
|
||||||
|
|
@ -323,7 +327,7 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'PAYLOAD PREVIEW',
|
l.approvalsPayloadPreview,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -362,7 +366,7 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: onReject,
|
onPressed: onReject,
|
||||||
icon: const Icon(Icons.close, size: 16),
|
icon: const Icon(Icons.close, size: 16),
|
||||||
label: const Text('Reject'),
|
label: Text(l.approvalsRejectButton),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: theme.colorScheme.error,
|
foregroundColor: theme.colorScheme.error,
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
|
|
@ -374,7 +378,7 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: onApprove,
|
onPressed: onApprove,
|
||||||
icon: const Icon(Icons.check, size: 16),
|
icon: const Icon(Icons.check, size: 16),
|
||||||
label: const Text('Approve'),
|
label: Text(l.approvalsApproveButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -393,13 +397,13 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _expiresIn(DateTime t) {
|
String _expiresInLabel(BuildContext context, DateTime t) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final remaining = t.difference(DateTime.now());
|
final remaining = t.difference(DateTime.now());
|
||||||
if (remaining.isNegative) return 'expired';
|
if (remaining.isNegative) return l.approvalsPillExpired;
|
||||||
final minutes = remaining.inMinutes;
|
final minutes = remaining.inMinutes;
|
||||||
if (minutes < 60) return '${minutes}m';
|
if (minutes < 60) return l.approvalsExpiresIn(minutes);
|
||||||
final hours = remaining.inHours;
|
return l.approvalsExpiresInHours(remaining.inHours);
|
||||||
return '${hours}h';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,7 +418,7 @@ class _HistoryRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final (tone, icon, label) = _statusPresentation(record.status);
|
final (tone, icon, label) = _statusPresentation(context, record.status);
|
||||||
final decided = record.decidedAt;
|
final decided = record.decidedAt;
|
||||||
final timeStr =
|
final timeStr =
|
||||||
decided == null ? '' : _formatTimestamp(decided.toLocal());
|
decided == null ? '' : _formatTimestamp(decided.toLocal());
|
||||||
|
|
@ -467,14 +471,22 @@ class _HistoryRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static (FaiPillTone, IconData, String) _statusPresentation(String status) {
|
static (FaiPillTone, IconData, String) _statusPresentation(
|
||||||
|
BuildContext context,
|
||||||
|
String status,
|
||||||
|
) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case 'approved':
|
||||||
return (FaiPillTone.success, Icons.check, 'approved');
|
return (FaiPillTone.success, Icons.check, l.approvalsPillApproved);
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
return (FaiPillTone.danger, Icons.close, 'rejected');
|
return (FaiPillTone.danger, Icons.close, l.approvalsPillRejected);
|
||||||
case 'expired':
|
case 'expired':
|
||||||
return (FaiPillTone.neutral, Icons.timer_off_outlined, 'expired');
|
return (
|
||||||
|
FaiPillTone.neutral,
|
||||||
|
Icons.timer_off_outlined,
|
||||||
|
l.approvalsPillExpired,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (FaiPillTone.neutral, Icons.help_outline, status);
|
return (FaiPillTone.neutral, Icons.help_outline, status);
|
||||||
}
|
}
|
||||||
|
|
@ -488,7 +500,8 @@ class _HistoryDialog extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final (tone, icon, label) = _HistoryRow._statusPresentation(record.status);
|
final (tone, icon, label) =
|
||||||
|
_HistoryRow._statusPresentation(context, record.status);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -511,16 +524,17 @@ class _HistoryDialog extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (record.decidedAt != null)
|
if (record.decidedAt != null)
|
||||||
_kv(theme, 'decided',
|
_kv(theme, AppLocalizations.of(context)!.approvalsDialogDecided,
|
||||||
'${_formatTimestamp(record.decidedAt!.toLocal())} '
|
'${_formatTimestamp(record.decidedAt!.toLocal())} '
|
||||||
'· by ${record.decidedBy.isEmpty ? "(unknown)" : record.decidedBy}'),
|
'· ${record.decidedBy.isEmpty ? "(unknown)" : record.decidedBy}'),
|
||||||
_kv(theme, 'created',
|
_kv(theme, AppLocalizations.of(context)!.approvalsDialogCreated,
|
||||||
_formatTimestamp(record.createdAt.toLocal())),
|
_formatTimestamp(record.createdAt.toLocal())),
|
||||||
if (record.reason.isNotEmpty)
|
if (record.reason.isNotEmpty)
|
||||||
_kv(theme, 'reason', record.reason),
|
_kv(theme, AppLocalizations.of(context)!.approvalsDialogReason,
|
||||||
|
record.reason),
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
Text(
|
Text(
|
||||||
'PROMPT',
|
AppLocalizations.of(context)!.approvalsDialogPrompt,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -534,7 +548,7 @@ class _HistoryDialog extends StatelessWidget {
|
||||||
if (record.payloadPreview != null) ...[
|
if (record.payloadPreview != null) ...[
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
Text(
|
Text(
|
||||||
'PAYLOAD PREVIEW',
|
AppLocalizations.of(context)!.approvalsPayloadPreview,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -570,7 +584,7 @@ class _HistoryDialog extends StatelessWidget {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Close'),
|
child: Text(AppLocalizations.of(context)!.buttonClose),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onClearPressed() async {
|
Future<void> _onClearPressed() async {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final outcome = await _ClearAuditDialog.show(context);
|
final outcome = await _ClearAuditDialog.show(context);
|
||||||
if (outcome == null || !mounted) return;
|
if (outcome == null || !mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -50,18 +51,13 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(l.auditClearedToast(r.purged, r.channel))),
|
||||||
content: Text(
|
|
||||||
'Cleared ${r.purged} events on channel "${r.channel}". '
|
|
||||||
'Chain reseeded with marker.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Clear failed: ${_friendly(e)}')),
|
SnackBar(content: Text(l.auditClearFailed(_friendly(e)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +111,7 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_sweep_outlined, size: 18),
|
icon: const Icon(Icons.delete_sweep_outlined, size: 18),
|
||||||
tooltip: 'Clear log (local/dev channels only)',
|
tooltip: AppLocalizations.of(context)!.auditClearLogTooltip,
|
||||||
onPressed: _onClearPressed,
|
onPressed: _onClearPressed,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
|
@ -131,15 +127,15 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
? FaiEmptyState(
|
? FaiEmptyState(
|
||||||
icon: Icons.cloud_off_outlined,
|
icon: Icons.cloud_off_outlined,
|
||||||
iconColor: theme.colorScheme.error,
|
iconColor: theme.colorScheme.error,
|
||||||
title: 'Hub unreachable',
|
title: AppLocalizations.of(context)!.hubUnreachable,
|
||||||
hint: 'Start the hub with `fai serve`.',
|
hint: AppLocalizations.of(context)!.hubUnreachableHint,
|
||||||
)
|
)
|
||||||
: filtered.isEmpty
|
: filtered.isEmpty
|
||||||
? const FaiEmptyState(
|
? FaiEmptyState(
|
||||||
icon: Icons.timeline_outlined,
|
icon: Icons.timeline_outlined,
|
||||||
title: 'No events yet',
|
title: AppLocalizations.of(context)!.auditNoEvents,
|
||||||
hint:
|
hint:
|
||||||
'Run a flow to populate the audit stream.\nEvents appear here within seconds.',
|
AppLocalizations.of(context)!.auditNoEventsHint,
|
||||||
)
|
)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
|
|
@ -220,11 +216,12 @@ class _FilterChips extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const items = [
|
final l = AppLocalizations.of(context)!;
|
||||||
('all', 'all'),
|
final items = [
|
||||||
('flow.', 'flow'),
|
('all', l.auditFilterAll),
|
||||||
('step.', 'step'),
|
('flow.', l.auditFilterFlow),
|
||||||
('module.', 'module'),
|
('step.', l.auditFilterStep),
|
||||||
|
('module.', l.auditFilterModule),
|
||||||
];
|
];
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -317,6 +314,7 @@ class _LiveStatusBar extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final live = error == null;
|
final live = error == null;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -339,8 +337,8 @@ class _LiveStatusBar extends StatelessWidget {
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
live
|
live
|
||||||
? 'live · polling 2s · $eventCount event${eventCount == 1 ? '' : 's'}'
|
? l.auditLiveStatus(eventCount)
|
||||||
: 'disconnected · $error',
|
: l.auditDisconnected(error!),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -356,7 +354,7 @@ class _LiveStatusBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'hash chain verified',
|
l.auditHashChainVerified,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
@ -445,6 +443,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
String prettyDetail() {
|
String prettyDetail() {
|
||||||
final raw = event.detail;
|
final raw = event.detail;
|
||||||
if (raw == null || raw.isEmpty) return '';
|
if (raw == null || raw.isEmpty) return '';
|
||||||
|
|
@ -511,7 +510,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
||||||
if (detail.isNotEmpty) ...[
|
if (detail.isNotEmpty) ...[
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
Text(
|
Text(
|
||||||
'DETAIL',
|
l.auditDetailHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -556,24 +555,24 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
||||||
icon: const Icon(Icons.auto_awesome, size: 16),
|
icon: const Icon(Icons.auto_awesome, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
_explaining
|
_explaining
|
||||||
? 'Asking…'
|
? l.auditAsking
|
||||||
: _explanation == null
|
: _explanation == null
|
||||||
? 'Explain'
|
? l.auditExplain
|
||||||
: 'Re-ask',
|
: l.auditReask,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (_aiStatus != null && !_aiStatus!.enabled && (event.error != null))
|
else if (_aiStatus != null && !_aiStatus!.enabled && (event.error != null))
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: 'Configure System AI in Settings (Cmd+,)',
|
message: l.auditConfigureSystemAi,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
icon: const Icon(Icons.auto_awesome_outlined, size: 16),
|
icon: const Icon(Icons.auto_awesome_outlined, size: 16),
|
||||||
label: const Text('Explain'),
|
label: Text(l.auditExplain),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Close'),
|
child: Text(l.buttonClose),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -648,6 +647,7 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final ok = result?.isSuccess ?? false;
|
final ok = result?.isSuccess ?? false;
|
||||||
final color = explaining
|
final color = explaining
|
||||||
? theme.colorScheme.primary
|
? theme.colorScheme.primary
|
||||||
|
|
@ -670,7 +670,7 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
Icon(Icons.auto_awesome, size: 14, color: color),
|
Icon(Icons.auto_awesome, size: 14, color: color),
|
||||||
const SizedBox(width: FaiSpace.xs),
|
const SizedBox(width: FaiSpace.xs),
|
||||||
Text(
|
Text(
|
||||||
'SYSTEM AI',
|
l.auditSystemAi,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -686,12 +686,16 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
if (result != null && result!.isSuccess && result!.cached) ...[
|
if (result != null && result!.isSuccess && result!.cached) ...[
|
||||||
const SizedBox(width: FaiSpace.xs),
|
const SizedBox(width: FaiSpace.xs),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message:
|
message: () {
|
||||||
'Served from the local cache. Originally generated '
|
final hits = result!.cacheHits > 1
|
||||||
'${result!.cachedAt.isEmpty ? "at unknown time" : "on ${result!.cachedAt}"}'
|
? l.auditCachedTooltipHits(result!.cacheHits)
|
||||||
'${result!.cacheHits > 1 ? " · ${result!.cacheHits} hits" : ""}.',
|
: '';
|
||||||
child: const FaiPill(
|
return result!.cachedAt.isEmpty
|
||||||
label: 'cached',
|
? l.auditCachedTooltipUnknown(hits)
|
||||||
|
: l.auditCachedTooltipKnown(result!.cachedAt, hits);
|
||||||
|
}(),
|
||||||
|
child: FaiPill(
|
||||||
|
label: l.auditCachedPill,
|
||||||
tone: FaiPillTone.success,
|
tone: FaiPillTone.success,
|
||||||
icon: Icons.bolt,
|
icon: Icons.bolt,
|
||||||
),
|
),
|
||||||
|
|
@ -701,8 +705,8 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
if (result != null && result!.isSuccess && result!.latencyMs > 0)
|
if (result != null && result!.isSuccess && result!.latencyMs > 0)
|
||||||
Text(
|
Text(
|
||||||
result!.cached
|
result!.cached
|
||||||
? 'orig ${result!.latencyMs} ms'
|
? l.auditOriginalLatency(result!.latencyMs)
|
||||||
: '${result!.latencyMs} ms',
|
: l.auditLatency(result!.latencyMs),
|
||||||
style: FaiTheme.mono(
|
style: FaiTheme.mono(
|
||||||
size: 10,
|
size: 10,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -711,7 +715,7 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
if (result != null && result!.isSuccess && onRegenerate != null) ...[
|
if (result != null && result!.isSuccess && onRegenerate != null) ...[
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: 'Regenerate — skip cache, ask the model again',
|
message: l.auditRegenerateTooltip,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 14),
|
icon: const Icon(Icons.refresh, size: 14),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
|
@ -732,7 +736,7 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
'Asking the configured System AI…',
|
l.auditAskingFull,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -749,7 +753,7 @@ class _ExplanationPanel extends StatelessWidget {
|
||||||
if (result!.fixHint.isNotEmpty) ...[
|
if (result!.fixHint.isNotEmpty) ...[
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
'FIX',
|
l.auditFixLabel,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -820,8 +824,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Clear audit log?'),
|
title: Text(l.auditClearDialogTitle),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 460),
|
constraints: const BoxConstraints(maxWidth: 460),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -829,9 +834,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Wipes every audit event on the active channel and seeds a '
|
l.auditClearDialogBody,
|
||||||
'fresh chain.reset marker carrying reviewer + reason. '
|
|
||||||
'Refused on beta / production. Irreversible.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -839,9 +842,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _reviewer,
|
controller: _reviewer,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Reviewer',
|
labelText: l.auditClearReviewerLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -852,9 +855,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
controller: _reason,
|
controller: _reason,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Reason (recorded in chain.reset marker)',
|
labelText: l.auditClearReasonLabel,
|
||||||
helperText: _reason.text.trim().isEmpty
|
helperText: _reason.text.trim().isEmpty
|
||||||
? 'Required — auditors will read this.'
|
? l.auditClearReasonHelper
|
||||||
: null,
|
: null,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
|
@ -867,7 +870,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, null),
|
onPressed: () => Navigator.pop(context, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
ValueListenableBuilder<TextEditingValue>(
|
ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: _reason,
|
valueListenable: _reason,
|
||||||
|
|
@ -885,7 +888,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||||
),
|
),
|
||||||
child: const Text('Clear log'),
|
child: Text(l.auditClearLogButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
import '../data/system_actions.dart';
|
import '../data/system_actions.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
import 'fai_pill.dart';
|
import 'fai_pill.dart';
|
||||||
|
|
@ -71,13 +72,15 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _n8nEndpoints = list);
|
setState(() => _n8nEndpoints = list);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Added n8n endpoint "${draft.name}".')),
|
SnackBar(content: Text(l.addedN8nToast(draft.name))),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Add failed: $e')),
|
SnackBar(content: Text(l.addFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,8 +92,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
setState(() => _n8nEndpoints = list);
|
setState(() => _n8nEndpoints = list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Remove failed: $e')),
|
SnackBar(content: Text(l.removeFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,8 +106,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
setState(() => _n8nEndpoints = list);
|
setState(() => _n8nEndpoints = list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Refresh failed: $e')),
|
SnackBar(content: Text(l.refreshFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,13 +133,15 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _mcpClients = list);
|
setState(() => _mcpClients = list);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Added MCP server "${draft.name}".')),
|
SnackBar(content: Text(l.addedMcpToast(draft.name))),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Add failed: $e')),
|
SnackBar(content: Text(l.addFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,8 +153,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
setState(() => _mcpClients = list);
|
setState(() => _mcpClients = list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Remove failed: $e')),
|
SnackBar(content: Text(l.removeFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,8 +167,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
setState(() => _mcpClients = list);
|
setState(() => _mcpClients = list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Refresh failed: $e')),
|
SnackBar(content: Text(l.refreshFailedToast(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,9 +253,10 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
});
|
});
|
||||||
final port = int.tryParse(_port.text.trim());
|
final port = int.tryParse(_port.text.trim());
|
||||||
if (port == null || port <= 0 || port > 65535) {
|
if (port == null || port <= 0 || port > 65535) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
setState(() {
|
setState(() {
|
||||||
_saving = false;
|
_saving = false;
|
||||||
_error = 'port must be 1–65535';
|
_error = l.settingsPortError;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -270,8 +280,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Hub endpoint'),
|
title: Text(l.settingsTitle),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
),
|
),
|
||||||
|
|
@ -287,7 +298,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Where should Studio connect? Default is the local hub at 127.0.0.1:50051.',
|
l.settingsHubEndpointHint,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -295,9 +306,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _host,
|
controller: _host,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Host',
|
labelText: l.settingsHost,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -309,9 +320,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _port,
|
controller: _port,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Port',
|
labelText: l.settingsPort,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
|
@ -323,9 +334,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('TLS'),
|
title: Text(l.settingsTls),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'https/grpc-secure',
|
l.settingsTlsSubtitle,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -373,7 +384,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
if (_channels != null) ...[
|
if (_channels != null) ...[
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
Text(
|
Text(
|
||||||
'CHANNELS',
|
l.channelsHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -382,8 +393,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'Switch hub channel (writes ~/.fai/current-channel and '
|
l.channelsBlurb,
|
||||||
'restarts the daemon). Connect just changes Studio\'s wire.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -464,7 +474,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _saving ? null : () => Navigator.pop(context, false),
|
onPressed: _saving ? null : () => Navigator.pop(context, false),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _saving ? null : _save,
|
onPressed: _saving ? null : _save,
|
||||||
|
|
@ -474,7 +484,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Save & connect'),
|
: Text(l.settingsSaveAndConnect),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -512,6 +522,7 @@ class _ChannelRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -547,19 +558,19 @@ class _ChannelRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
channel.running ? 'running' : 'stopped',
|
channel.running ? l.channelsRunning : l.channelsStopped,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (active)
|
if (active)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(right: FaiSpace.sm),
|
padding: const EdgeInsets.only(right: FaiSpace.sm),
|
||||||
child: FaiPill(label: 'active', tone: FaiPillTone.success),
|
child: FaiPill(label: l.channelsActive, tone: FaiPillTone.success),
|
||||||
),
|
),
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: 'Channel actions',
|
tooltip: l.channelsActionsTooltip,
|
||||||
icon: const Icon(Icons.more_vert, size: 18),
|
icon: const Icon(Icons.more_vert, size: 18),
|
||||||
onSelected: (v) {
|
onSelected: (v) {
|
||||||
switch (v) {
|
switch (v) {
|
||||||
|
|
@ -581,34 +592,34 @@ class _ChannelRow extends StatelessWidget {
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'connect',
|
value: 'connect',
|
||||||
enabled: channel.running && onConnect != null,
|
enabled: channel.running && onConnect != null,
|
||||||
child: const _MenuRow(
|
child: _MenuRow(
|
||||||
icon: Icons.link,
|
icon: Icons.link,
|
||||||
text: 'Connect Studio to this channel',
|
text: l.channelsConnect,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'switch',
|
value: 'switch',
|
||||||
enabled: !active && onSwitch != null,
|
enabled: !active && onSwitch != null,
|
||||||
child: const _MenuRow(
|
child: _MenuRow(
|
||||||
icon: Icons.swap_horiz,
|
icon: Icons.swap_horiz,
|
||||||
text: 'Make this the active channel',
|
text: l.channelsSwitch,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'enable',
|
value: 'enable',
|
||||||
enabled: onEnableAutostart != null,
|
enabled: onEnableAutostart != null,
|
||||||
child: const _MenuRow(
|
child: _MenuRow(
|
||||||
icon: Icons.play_circle_outline,
|
icon: Icons.play_circle_outline,
|
||||||
text: 'Enable autostart at login',
|
text: l.channelsEnableAutostart,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'disable',
|
value: 'disable',
|
||||||
enabled: onDisableAutostart != null,
|
enabled: onDisableAutostart != null,
|
||||||
child: const _MenuRow(
|
child: _MenuRow(
|
||||||
icon: Icons.pause_circle_outline,
|
icon: Icons.pause_circle_outline,
|
||||||
text: 'Disable autostart',
|
text: l.channelsDisableAutostart,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -645,13 +656,14 @@ class _SystemAiPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'SYSTEM AI',
|
l.systemAiHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -660,7 +672,7 @@ class _SystemAiPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: status.enabled ? 'enabled' : 'off',
|
label: status.enabled ? l.systemAiEnabled : l.systemAiOff,
|
||||||
tone: status.enabled ? FaiPillTone.success : FaiPillTone.neutral,
|
tone: status.enabled ? FaiPillTone.success : FaiPillTone.neutral,
|
||||||
),
|
),
|
||||||
if (status.enabled) ...[
|
if (status.enabled) ...[
|
||||||
|
|
@ -676,7 +688,7 @@ class _SystemAiPanel extends StatelessWidget {
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onEdit,
|
onPressed: onEdit,
|
||||||
icon: const Icon(Icons.edit_outlined, size: 14),
|
icon: const Icon(Icons.edit_outlined, size: 14),
|
||||||
label: Text(status.enabled ? 'Edit…' : 'Configure…'),
|
label: Text(status.enabled ? l.systemAiEdit : l.systemAiConfigure),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -691,10 +703,7 @@ class _SystemAiPanel extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Off — failure explanations on the audit page are disabled. '
|
l.systemAiOffBlurb,
|
||||||
'Click Configure… to pick a provider (Ollama / OpenAI / LM '
|
|
||||||
'Studio / vLLM / Custom). Operator config is rewritten in place; '
|
|
||||||
'no daemon restart needed.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -765,10 +774,9 @@ class McpClientDraft {
|
||||||
|
|
||||||
/// MCP-clients panel — shows every configured server with its
|
/// MCP-clients panel — shows every configured server with its
|
||||||
/// last-discovery health, plus Add / Remove / Refresh actions.
|
/// last-discovery health, plus Add / Remove / Refresh actions.
|
||||||
/// This is *the* surface that wires the viral-bridge story to
|
/// Configure a server here, the store gains synthetic
|
||||||
/// the operator: configure a server here, watch the store
|
/// capabilities for every advertised tool. Mirrors `fai mcp …`
|
||||||
/// fill with synthetic capabilities. Mirrors `fai mcp …` on
|
/// on the CLI.
|
||||||
/// the CLI.
|
|
||||||
class _McpClientsPanel extends StatelessWidget {
|
class _McpClientsPanel extends StatelessWidget {
|
||||||
final List<McpClientInfo> clients;
|
final List<McpClientInfo> clients;
|
||||||
final ValueChanged<McpClientDraft> onAdd;
|
final ValueChanged<McpClientDraft> onAdd;
|
||||||
|
|
@ -785,13 +793,14 @@ class _McpClientsPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'MCP CLIENTS',
|
l.mcpHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -800,19 +809,19 @@ class _McpClientsPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: '${clients.length} server${clients.length == 1 ? "" : "s"}',
|
label: l.mcpServersCount(clients.length),
|
||||||
tone: FaiPillTone.neutral,
|
tone: FaiPillTone.neutral,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 16),
|
icon: const Icon(Icons.refresh, size: 16),
|
||||||
tooltip: 'Re-run discovery',
|
tooltip: l.mcpRediscoverTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: onRefresh,
|
onPressed: onRefresh,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add, size: 16),
|
icon: const Icon(Icons.add, size: 16),
|
||||||
tooltip: 'Add MCP server',
|
tooltip: l.mcpAddTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final draft = await _AddMcpClientDialog.show(context);
|
final draft = await _AddMcpClientDialog.show(context);
|
||||||
|
|
@ -823,8 +832,7 @@ class _McpClientsPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'External MCP servers federate their tools as `mcp.<server>.<tool>` capabilities. '
|
l.mcpHint,
|
||||||
'Configure once, see them in the Store.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -834,7 +842,7 @@ class _McpClientsPanel extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
||||||
child: Text(
|
child: Text(
|
||||||
'No MCP servers configured. Click + to add one.',
|
l.mcpEmpty,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -859,6 +867,7 @@ class _McpClientRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final ok = client.healthy;
|
final ok = client.healthy;
|
||||||
final dotColor = client.errorKind.isEmpty
|
final dotColor = client.errorKind.isEmpty
|
||||||
? (ok ? FaiColors.success : FaiColors.muted)
|
? (ok ? FaiColors.success : FaiColors.muted)
|
||||||
|
|
@ -890,7 +899,7 @@ class _McpClientRow extends StatelessWidget {
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
if (client.toolCount > 0)
|
if (client.toolCount > 0)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: '${client.toolCount} tools',
|
label: l.mcpToolsCount(client.toolCount),
|
||||||
tone: FaiPillTone.success,
|
tone: FaiPillTone.success,
|
||||||
)
|
)
|
||||||
else if (client.errorKind.isNotEmpty)
|
else if (client.errorKind.isNotEmpty)
|
||||||
|
|
@ -921,7 +930,7 @@ class _McpClientRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, size: 14),
|
icon: const Icon(Icons.delete_outline, size: 14),
|
||||||
tooltip: 'Remove server',
|
tooltip: l.mcpRemoveTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: onRemove,
|
onPressed: onRemove,
|
||||||
),
|
),
|
||||||
|
|
@ -951,6 +960,17 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
|
||||||
final _apiKey = TextEditingController();
|
final _apiKey = TextEditingController();
|
||||||
final _desc = TextEditingController();
|
final _desc = TextEditingController();
|
||||||
|
|
||||||
|
/// Pick a suggestion → fill the form. Operators still hit
|
||||||
|
/// "Add + discover" so nothing happens unprompted.
|
||||||
|
void _applySuggestion(_McpSuggestion s) {
|
||||||
|
setState(() {
|
||||||
|
_name.text = s.name;
|
||||||
|
_endpoint.text = s.endpoint;
|
||||||
|
_apiKey.text = s.apiKeyEnv;
|
||||||
|
_desc.text = s.description;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_name.dispose();
|
_name.dispose();
|
||||||
|
|
@ -963,59 +983,85 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Add MCP server'),
|
title: Text(l.addMcpTitle),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Tools the server advertises via `tools/list` appear in the Store as '
|
l.addMcpIntro,
|
||||||
'`mcp.<name>.<tool>` capabilities.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
// Curated suggestions from the official Anthropic
|
||||||
|
// MCP servers + a few community staples. Click a
|
||||||
|
// chip to pre-fill the form. The operator still
|
||||||
|
// confirms via "Add + discover" — no unprompted
|
||||||
|
// subprocess spawn.
|
||||||
|
Text(
|
||||||
|
l.addMcpSuggestionsLabel,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: FaiSpace.xs,
|
||||||
|
runSpacing: FaiSpace.xs,
|
||||||
|
children: [
|
||||||
|
for (final s in _kMcpSuggestions)
|
||||||
|
ActionChip(
|
||||||
|
avatar: Icon(s.icon, size: 14),
|
||||||
|
label: Text(s.name),
|
||||||
|
onPressed: () => _applySuggestion(s),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: FaiSpace.xl),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _name,
|
controller: _name,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Name (no dots)',
|
labelText: l.addMcpNameLabel,
|
||||||
hintText: 'filesystem',
|
hintText: l.addMcpNameHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _endpoint,
|
controller: _endpoint,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Endpoint',
|
labelText: l.addMcpEndpointLabel,
|
||||||
hintText: 'http://127.0.0.1:3001/mcp',
|
hintText: l.addMcpEndpointHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _apiKey,
|
controller: _apiKey,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'API key env var (optional)',
|
labelText: l.addMcpApiKeyLabel,
|
||||||
hintText: 'MCP_FILESYSTEM_TOKEN',
|
hintText: l.addMcpApiKeyHint,
|
||||||
prefixText: '\$',
|
prefixText: '\$',
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _desc,
|
controller: _desc,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Notes (optional)',
|
labelText: l.addMcpNotesLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1025,7 +1071,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, null),
|
onPressed: () => Navigator.pop(context, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|
@ -1042,7 +1088,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Add + discover'),
|
child: Text(l.addMcpAddButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -1081,13 +1127,14 @@ class _N8nEndpointsPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'N8N ENDPOINTS',
|
l.n8nHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -1096,20 +1143,19 @@ class _N8nEndpointsPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label:
|
label: l.n8nEndpointsCount(endpoints.length),
|
||||||
'${endpoints.length} endpoint${endpoints.length == 1 ? "" : "s"}',
|
|
||||||
tone: FaiPillTone.neutral,
|
tone: FaiPillTone.neutral,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 16),
|
icon: const Icon(Icons.refresh, size: 16),
|
||||||
tooltip: 'Re-run discovery',
|
tooltip: l.n8nRediscoverTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: onRefresh,
|
onPressed: onRefresh,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add, size: 16),
|
icon: const Icon(Icons.add, size: 16),
|
||||||
tooltip: 'Add n8n endpoint',
|
tooltip: l.n8nAddTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final draft = await _AddN8nEndpointDialog.show(context);
|
final draft = await _AddN8nEndpointDialog.show(context);
|
||||||
|
|
@ -1120,8 +1166,7 @@ class _N8nEndpointsPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'Active n8n workflows surface as `n8n.<endpoint>.<slug>` capabilities. '
|
l.n8nHint,
|
||||||
'Configure once, see them in the Store.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1131,7 +1176,7 @@ class _N8nEndpointsPanel extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
||||||
child: Text(
|
child: Text(
|
||||||
'No n8n endpoints configured. Click + to add one.',
|
l.n8nEmpty,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1156,6 +1201,7 @@ class _N8nEndpointRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final ok = endpoint.healthy;
|
final ok = endpoint.healthy;
|
||||||
final dotColor = endpoint.errorKind.isEmpty
|
final dotColor = endpoint.errorKind.isEmpty
|
||||||
? (ok ? FaiColors.success : FaiColors.muted)
|
? (ok ? FaiColors.success : FaiColors.muted)
|
||||||
|
|
@ -1187,7 +1233,7 @@ class _N8nEndpointRow extends StatelessWidget {
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
if (endpoint.workflowCount > 0)
|
if (endpoint.workflowCount > 0)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: '${endpoint.workflowCount} workflows',
|
label: l.n8nWorkflowsCount(endpoint.workflowCount),
|
||||||
tone: FaiPillTone.success,
|
tone: FaiPillTone.success,
|
||||||
)
|
)
|
||||||
else if (endpoint.errorKind.isNotEmpty)
|
else if (endpoint.errorKind.isNotEmpty)
|
||||||
|
|
@ -1218,7 +1264,7 @@ class _N8nEndpointRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, size: 14),
|
icon: const Icon(Icons.delete_outline, size: 14),
|
||||||
tooltip: 'Remove endpoint',
|
tooltip: l.n8nRemoveTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: onRemove,
|
onPressed: onRemove,
|
||||||
),
|
),
|
||||||
|
|
@ -1260,8 +1306,9 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Add n8n endpoint'),
|
title: Text(l.addN8nTitle),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -1269,8 +1316,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Active workflows surface in the Store as '
|
l.addN8nIntro,
|
||||||
'`n8n.<name>.<workflow_slug>` capabilities.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1279,40 +1325,40 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: _name,
|
controller: _name,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Name (no dots)',
|
labelText: l.addN8nNameLabel,
|
||||||
hintText: 'ops',
|
hintText: l.addN8nNameHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _baseUrl,
|
controller: _baseUrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Base URL (without /api/v1)',
|
labelText: l.addN8nBaseUrlLabel,
|
||||||
hintText: 'https://n8n.example.com',
|
hintText: l.addN8nBaseUrlHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _apiKey,
|
controller: _apiKey,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'API key env var (optional)',
|
labelText: l.addN8nApiKeyLabel,
|
||||||
hintText: 'N8N_OPS_KEY',
|
hintText: l.addN8nApiKeyHint,
|
||||||
prefixText: '\$',
|
prefixText: '\$',
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _desc,
|
controller: _desc,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Notes (optional)',
|
labelText: l.addN8nNotesLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1322,7 +1368,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, null),
|
onPressed: () => Navigator.pop(context, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|
@ -1337,9 +1383,106 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Add + discover'),
|
child: Text(l.addN8nAddButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One curated MCP-server suggestion — the operator clicks
|
||||||
|
/// the chip to pre-fill the add-dialog. Strictly informational
|
||||||
|
/// metadata; nothing is auto-spawned. Trust model:
|
||||||
|
/// referencing public projects with their canonical install
|
||||||
|
/// command is the same legal / security shape as a package
|
||||||
|
/// manager listing a third-party repo.
|
||||||
|
///
|
||||||
|
/// All entries point at official Anthropic MCP servers from
|
||||||
|
/// `github.com/modelcontextprotocol/servers` plus a few
|
||||||
|
/// community-curated standards. Refresh the list when the
|
||||||
|
/// upstream README changes.
|
||||||
|
class _McpSuggestion {
|
||||||
|
final String name;
|
||||||
|
final IconData icon;
|
||||||
|
final String endpoint;
|
||||||
|
final String apiKeyEnv;
|
||||||
|
final String description;
|
||||||
|
const _McpSuggestion({
|
||||||
|
required this.name,
|
||||||
|
required this.icon,
|
||||||
|
required this.endpoint,
|
||||||
|
required this.apiKeyEnv,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const _kMcpSuggestions = <_McpSuggestion>[
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'filesystem',
|
||||||
|
icon: Icons.folder_outlined,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-filesystem /tmp',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description:
|
||||||
|
'Anthropic — read/write files in /tmp. Edit the path before saving.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'fetch',
|
||||||
|
icon: Icons.cloud_download_outlined,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-fetch',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description: 'Anthropic — fetch arbitrary HTTP(S) URLs as markdown.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'github',
|
||||||
|
icon: Icons.code,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-github',
|
||||||
|
apiKeyEnv: 'GITHUB_PERSONAL_ACCESS_TOKEN',
|
||||||
|
description:
|
||||||
|
'Anthropic — issues, PRs, repo files. Needs a GitHub PAT in the env var.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'puppeteer',
|
||||||
|
icon: Icons.web,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-puppeteer',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description: 'Anthropic — headless browser automation for screenshots / scraping.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'postgres',
|
||||||
|
icon: Icons.storage_outlined,
|
||||||
|
endpoint:
|
||||||
|
'stdio://npx -y @modelcontextprotocol/server-postgres postgresql://user:pw@host/db',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description:
|
||||||
|
'Anthropic — read-only SQL queries against a Postgres database. Edit the URL.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'sqlite',
|
||||||
|
icon: Icons.dataset_outlined,
|
||||||
|
endpoint:
|
||||||
|
'stdio://npx -y @modelcontextprotocol/server-sqlite --db-path /tmp/db.sqlite',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description: 'Anthropic — query a local SQLite file. Edit the path.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'brave-search',
|
||||||
|
icon: Icons.search,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-brave-search',
|
||||||
|
apiKeyEnv: 'BRAVE_API_KEY',
|
||||||
|
description: 'Anthropic — web search via Brave. API key required.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'memory',
|
||||||
|
icon: Icons.psychology_outlined,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-memory',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description: 'Anthropic — persistent knowledge graph for an agent\'s memory.',
|
||||||
|
),
|
||||||
|
_McpSuggestion(
|
||||||
|
name: 'time',
|
||||||
|
icon: Icons.schedule,
|
||||||
|
endpoint: 'stdio://npx -y @modelcontextprotocol/server-time',
|
||||||
|
apiKeyEnv: '',
|
||||||
|
description: 'Anthropic — current time + timezone conversion.',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -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.25.0
|
version: 0.26.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