mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
Merge pull request #24 from awizemann/multi-language
feat(i18n): close silently un-localizable sites (Phase 1b)
This commit is contained in:
+9
-47
@@ -47,56 +47,18 @@ For the three supported non-English locales we use Xcode's built-in AI translati
|
|||||||
|
|
||||||
Strings that are **user data** (session titles, memory file contents, log lines, shell commands shown in UI, file paths) should pass through without localization — this happens naturally when the value is a `String` variable, since those overloads skip the catalog.
|
Strings that are **user data** (session titles, memory file contents, log lines, shell commands shown in UI, file paths) should pass through without localization — this happens naturally when the value is a `String` variable, since those overloads skip the catalog.
|
||||||
|
|
||||||
## Outstanding audit follow-ups
|
## Audit status
|
||||||
|
|
||||||
The [initial i18n PR](#) enabled the catalog infrastructure and migrated clear locale-bug formatter sites. The following patterns remain in the codebase and should be refactored **before** translations ship in Phase 2 — otherwise their English copy will leak through regardless of locale:
|
Phase 1b (the `multi-language` PR) closed every tracked site from the original audit:
|
||||||
|
|
||||||
### Category A — UI copy held in a `String` variable (needs `LocalizedStringResource`)
|
- **Category A high-priority (ternary UI copy)** — converted to `Text`-ternary form so each branch routes through `LocalizedStringKey`.
|
||||||
|
- **Category A medium-priority (enum `.rawValue` displays)** — each enum now exposes `displayName: LocalizedStringResource` and call sites use it. `LogEntry.LogLevel` (technical jargon) stays verbatim.
|
||||||
|
- **Category A lower-priority (displayName passthroughs)** — wrapped with `Text(verbatim:)` for proper nouns / user data (`HermesToolPlatform`, `ServerRegistry.Entry`, `MCPServerPreset`). `MCPTransport.displayName` promoted to `LocalizedStringResource`.
|
||||||
|
- **Category B (composite format strings)** — migrated to `Text("\(arg) suffix")` with `LocalizedStringKey` or to `.percent` / `.currency` FormatStyle.
|
||||||
|
- **Category C (hard-coded day names)** — replaced with `Calendar.current.shortWeekdaySymbols`, re-indexed to match the existing Mon=0 data model.
|
||||||
|
- **Category D (`.help(stringVar)` sites)** — `ConnectionStatusPill` now returns `Text` from its `labelText` / `tooltipText` properties.
|
||||||
|
|
||||||
High-priority (ternary UI copy, visible status chrome):
|
If you spot a new silently-un-localizable site during translation review, prefer the patterns in the table above over one-off workarounds.
|
||||||
|
|
||||||
- `Features/Health/Views/HealthView.swift:135` — `"Hermes Running"` / `"Hermes Stopped"` ternary
|
|
||||||
- `Features/Chat/Views/ChatView.swift:125` — `"Active"` fallback
|
|
||||||
- `Features/Chat/Views/ChatView.swift:241` — `"Voice On" / "Voice Off"`
|
|
||||||
- `Features/Chat/Views/ChatView.swift:256` — `"TTS On" / "TTS Off"`
|
|
||||||
- `Features/Chat/Views/ChatView.swift:271` — `"Recording..." / "Push to Talk"`
|
|
||||||
- `Features/MCPServers/Views/MCPServerTestResultView.swift:13` — `"Test passed" / "Test failed"`
|
|
||||||
- `Features/Profiles/Views/ProfilesView.swift:145` — `"Active profile" / "Inactive"`
|
|
||||||
- `Features/Platforms/Views/PlatformSetup/SignalSetupView.swift:54` — `"signal-cli is available on PATH"` / not-found message
|
|
||||||
- `Features/QuickCommands/Views/QuickCommandsView.swift:148` — `"Add Quick Command"` / `"Edit /\(name)"` ternary
|
|
||||||
- `Features/MCPServers/Views/MCPServersView.swift:131` — `.help("\(count) tools")` vs `"Test failed"` ternary
|
|
||||||
- `Features/MCPServers/Views/MCPServerDetailView.swift:157, 185` — masked-value placeholder
|
|
||||||
|
|
||||||
Medium-priority (enum `.rawValue` → display):
|
|
||||||
|
|
||||||
- `Features/Logs/Views/LogsView.swift:30,38,48,69` — file / component / level display
|
|
||||||
- `Features/Projects/Views/ProjectsView.swift:153` — tab display
|
|
||||||
- `Features/Skills/Views/SkillsView.swift:37` — tab display
|
|
||||||
- `Features/Insights/Views/InsightsView.swift:40, 201` — period picker, day-of-week names (see also Category C)
|
|
||||||
- `Features/CredentialPools/Views/CredentialPoolsView.swift:265` — type display
|
|
||||||
- `Features/Activity/Views/ActivityView.swift:117` — kind display
|
|
||||||
|
|
||||||
Lower-priority (displayName passthroughs from services that could be wrapped once at the source):
|
|
||||||
|
|
||||||
- `Features/Platforms/Views/PlatformsView.swift:43, 91`, `Features/Tools/Views/ToolsView.swift:46` — `platform.displayName`
|
|
||||||
- `Features/Gateway/Views/GatewayView.swift:105` — `platform.name.capitalized`
|
|
||||||
- `Features/MCPServers/Views/*` — `transport.displayName`, preset names / descriptions
|
|
||||||
- `Features/Servers/Views/ServerSwitcherToolbar.swift:40`, `ManageServersView.swift:96` — `server.displayName`
|
|
||||||
|
|
||||||
### Category B — Composite format strings with translatable suffixes
|
|
||||||
|
|
||||||
- `Features/Settings/Views/Components/ModelPickerSheet.swift:105` — `ctx + " ctx"` — literal " ctx" needs keying
|
|
||||||
- `Features/Insights/Views/InsightsView.swift:93` — `formatTokens(...) + " tokens"` — literal " tokens" needs keying
|
|
||||||
- `Features/MCPServers/Views/MCPServerTestResultView.swift:15` — `"%.1fs · %d tools"` — needs splitting into a `String(localized: "\(elapsed)s · \(count) tools")`
|
|
||||||
- `Features/Insights/Views/InsightsView.swift:167` — `"%.1f%%"` — needs `.formatted(.percent)` after verifying the input range (0…1 vs 0…100)
|
|
||||||
|
|
||||||
### Category C — Hard-coded day / month name arrays
|
|
||||||
|
|
||||||
- `Features/Insights/Views/InsightsView.swift:196` — `["Mon", "Tue", …]` literal — use `Calendar.current.shortWeekdaySymbols` (locale-aware).
|
|
||||||
|
|
||||||
### Category D — `.help(stringVar)` sites
|
|
||||||
|
|
||||||
- `Features/Servers/Views/ConnectionStatusPill.swift:42` — `.help(tooltip)`; refactor `tooltip` to return `LocalizedStringKey` / `LocalizedStringResource`.
|
|
||||||
|
|
||||||
### Non-blocking (intentional verbatim)
|
### Non-blocking (intentional verbatim)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ enum MCPTransport: String, Sendable, Equatable, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: LocalizedStringResource {
|
||||||
switch self {
|
switch self {
|
||||||
case .stdio: return "Local (stdio)"
|
case .stdio: return "Local (stdio)"
|
||||||
case .http: return "Remote (HTTP)"
|
case .http: return "Remote (HTTP)"
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ enum ToolKind: String, Sendable, CaseIterable {
|
|||||||
case browser
|
case browser
|
||||||
case other
|
case other
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .read: return "Read"
|
||||||
|
case .edit: return "Edit"
|
||||||
|
case .execute: return "Execute"
|
||||||
|
case .fetch: return "Fetch"
|
||||||
|
case .browser: return "Browser"
|
||||||
|
case .other: return "Other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var icon: String {
|
var icon: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .read: return "doc.text.magnifyingglass"
|
case .read: return "doc.text.magnifyingglass"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ struct ActivityView: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(entry.toolName)
|
Text(entry.toolName)
|
||||||
.font(.title3.bold().monospaced())
|
.font(.title3.bold().monospaced())
|
||||||
Text(entry.kind.rawValue.capitalized)
|
Text(entry.kind.displayName)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ struct ChatView: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(.green)
|
.fill(.green)
|
||||||
.frame(width: 6, height: 6)
|
.frame(width: 6, height: 6)
|
||||||
Text(viewModel.acpStatus.isEmpty ? "Active" : viewModel.acpStatus)
|
(viewModel.acpStatus.isEmpty ? Text("Active") : Text(viewModel.acpStatus))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -238,7 +238,7 @@ struct ChatView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: viewModel.voiceEnabled ? "mic.fill" : "mic.slash")
|
Image(systemName: viewModel.voiceEnabled ? "mic.fill" : "mic.slash")
|
||||||
.foregroundStyle(viewModel.voiceEnabled ? .green : .secondary)
|
.foregroundStyle(viewModel.voiceEnabled ? .green : .secondary)
|
||||||
Text(viewModel.voiceEnabled ? "Voice On" : "Voice Off")
|
(viewModel.voiceEnabled ? Text("Voice On") : Text("Voice Off"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(viewModel.voiceEnabled ? .primary : .secondary)
|
.foregroundStyle(viewModel.voiceEnabled ? .primary : .secondary)
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ struct ChatView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: viewModel.ttsEnabled ? "speaker.wave.2.fill" : "speaker.slash")
|
Image(systemName: viewModel.ttsEnabled ? "speaker.wave.2.fill" : "speaker.slash")
|
||||||
.foregroundStyle(viewModel.ttsEnabled ? .green : .secondary)
|
.foregroundStyle(viewModel.ttsEnabled ? .green : .secondary)
|
||||||
Text(viewModel.ttsEnabled ? "TTS On" : "TTS Off")
|
(viewModel.ttsEnabled ? Text("TTS On") : Text("TTS Off"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(viewModel.ttsEnabled ? .primary : .secondary)
|
.foregroundStyle(viewModel.ttsEnabled ? .primary : .secondary)
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ struct ChatView: View {
|
|||||||
Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle")
|
Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle")
|
||||||
.foregroundStyle(viewModel.isRecording ? .red : Color.accentColor)
|
.foregroundStyle(viewModel.isRecording ? .red : Color.accentColor)
|
||||||
.symbolEffect(.pulse, isActive: viewModel.isRecording)
|
.symbolEffect(.pulse, isActive: viewModel.isRecording)
|
||||||
Text(viewModel.isRecording ? "Recording..." : "Push to Talk")
|
(viewModel.isRecording ? Text("Recording…") : Text("Push to Talk"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,13 @@ private struct AddCredentialSheet: View {
|
|||||||
case apiKey = "API Key"
|
case apiKey = "API Key"
|
||||||
case oauth = "OAuth"
|
case oauth = "OAuth"
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .apiKey: return "API Key"
|
||||||
|
case .oauth: return "OAuth"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var providerID: String = ""
|
@State private var providerID: String = ""
|
||||||
@@ -262,7 +269,7 @@ private struct AddCredentialSheet: View {
|
|||||||
Text("Credential Type").font(.caption).foregroundStyle(.secondary)
|
Text("Credential Type").font(.caption).foregroundStyle(.secondary)
|
||||||
Picker("", selection: $authType) {
|
Picker("", selection: $authType) {
|
||||||
ForEach(AuthType.allCases) { type in
|
ForEach(AuthType.allCases) { type in
|
||||||
Text(type.rawValue).tag(type)
|
Text(type.displayName).tag(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ struct GatewayView: View {
|
|||||||
Image(systemName: platform.icon)
|
Image(systemName: platform.icon)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundStyle(platform.isConnected ? Color.accentColor : .secondary)
|
.foregroundStyle(platform.isConnected ? Color.accentColor : .secondary)
|
||||||
Text(platform.name.capitalized)
|
Text(verbatim: platform.name.capitalized)
|
||||||
.font(.caption.bold())
|
.font(.caption.bold())
|
||||||
StatusBadge(
|
StatusBadge(
|
||||||
label: platform.state,
|
label: platform.state,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ struct HealthView: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(viewModel.hermesRunning ? .green : .red)
|
.fill(viewModel.hermesRunning ? .green : .red)
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
Text(viewModel.hermesRunning ? "Hermes Running" : "Hermes Stopped")
|
(viewModel.hermesRunning ? Text("Hermes Running") : Text("Hermes Stopped"))
|
||||||
.font(.caption.bold())
|
.font(.caption.bold())
|
||||||
if let pid = viewModel.hermesPID {
|
if let pid = viewModel.hermesPID {
|
||||||
Text("PID \(pid)")
|
Text("PID \(pid)")
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ enum InsightsPeriod: String, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .week: return "7 Days"
|
||||||
|
case .month: return "30 Days"
|
||||||
|
case .quarter: return "90 Days"
|
||||||
|
case .all: return "All Time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var sinceDate: Date {
|
var sinceDate: Date {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
switch self {
|
switch self {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct InsightsView: View {
|
|||||||
private var periodPicker: some View {
|
private var periodPicker: some View {
|
||||||
Picker("Period", selection: $viewModel.period) {
|
Picker("Period", selection: $viewModel.period) {
|
||||||
ForEach(InsightsPeriod.allCases) { period in
|
ForEach(InsightsPeriod.allCases) { period in
|
||||||
Text(period.rawValue).tag(period)
|
Text(period.displayName).tag(period)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
@@ -90,7 +90,7 @@ struct InsightsView: View {
|
|||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
Text("\(model.sessions) sessions")
|
Text("\(model.sessions) sessions")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
Text(formatTokens(model.totalTokens) + " tokens")
|
Text("\(formatTokens(model.totalTokens)) tokens")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ struct InsightsView: View {
|
|||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(width: 40, alignment: .trailing)
|
.frame(width: 40, alignment: .trailing)
|
||||||
Text(String(format: "%.1f%%", tool.percentage))
|
Text((tool.percentage / 100).formatted(.percent.precision(.fractionLength(1))))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
.frame(width: 50, alignment: .trailing)
|
.frame(width: 50, alignment: .trailing)
|
||||||
@@ -193,12 +193,12 @@ struct InsightsView: View {
|
|||||||
Text("By Day")
|
Text("By Day")
|
||||||
.font(.caption.bold())
|
.font(.caption.bold())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
let dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
let dayNames = Calendar.current.shortWeekdaySymbols
|
||||||
let maxVal = max(1, viewModel.dailyActivity.values.max() ?? 1)
|
let maxVal = max(1, viewModel.dailyActivity.values.max() ?? 1)
|
||||||
ForEach(0..<7, id: \.self) { day in
|
ForEach(0..<7, id: \.self) { day in
|
||||||
let count = viewModel.dailyActivity[day] ?? 0
|
let count = viewModel.dailyActivity[day] ?? 0
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(dayNames[day])
|
Text(verbatim: dayNames[(day + 1) % 7])
|
||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.frame(width: 30, alignment: .trailing)
|
.frame(width: 30, alignment: .trailing)
|
||||||
RoundedRectangle(cornerRadius: 2)
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ final class LogsViewModel {
|
|||||||
case gateway = "gateway.log"
|
case gateway = "gateway.log"
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .agent: return "Agent"
|
||||||
|
case .errors: return "Errors"
|
||||||
|
case .gateway: return "Gateway"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func path(for file: LogFile) -> String {
|
private func path(for file: LogFile) -> String {
|
||||||
@@ -43,6 +51,17 @@ final class LogsViewModel {
|
|||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .all: return "All"
|
||||||
|
case .gateway: return "Gateway"
|
||||||
|
case .agent: return "Agent"
|
||||||
|
case .tools: return "Tools"
|
||||||
|
case .cli: return "CLI"
|
||||||
|
case .cron: return "Cron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var loggerPrefix: String? {
|
var loggerPrefix: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .all: return nil
|
case .all: return nil
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ struct LogsView: View {
|
|||||||
set: { file in Task { await viewModel.switchLogFile(file) } }
|
set: { file in Task { await viewModel.switchLogFile(file) } }
|
||||||
)) {
|
)) {
|
||||||
ForEach(LogsViewModel.LogFile.allCases) { file in
|
ForEach(LogsViewModel.LogFile.allCases) { file in
|
||||||
Text(file.rawValue).tag(file)
|
Text(file.displayName).tag(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
@@ -35,7 +35,7 @@ struct LogsView: View {
|
|||||||
|
|
||||||
Picker("Component", selection: $viewModel.selectedComponent) {
|
Picker("Component", selection: $viewModel.selectedComponent) {
|
||||||
ForEach(LogsViewModel.LogComponent.allCases) { component in
|
ForEach(LogsViewModel.LogComponent.allCases) { component in
|
||||||
Text(component.rawValue).tag(component)
|
Text(component.displayName).tag(component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 140)
|
.frame(maxWidth: 140)
|
||||||
@@ -45,7 +45,7 @@ struct LogsView: View {
|
|||||||
Picker("Level", selection: $viewModel.filterLevel) {
|
Picker("Level", selection: $viewModel.filterLevel) {
|
||||||
Text("All Levels").tag(LogEntry.LogLevel?.none)
|
Text("All Levels").tag(LogEntry.LogLevel?.none)
|
||||||
ForEach(LogEntry.LogLevel.allCases, id: \.rawValue) { level in
|
ForEach(LogEntry.LogLevel.allCases, id: \.rawValue) { level in
|
||||||
Text(level.rawValue).tag(LogEntry.LogLevel?.some(level))
|
Text(verbatim: level.rawValue).tag(LogEntry.LogLevel?.some(level))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 150)
|
.frame(maxWidth: 150)
|
||||||
@@ -66,7 +66,7 @@ struct LogsView: View {
|
|||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(width: 140, alignment: .leading)
|
.frame(width: 140, alignment: .leading)
|
||||||
Text(entry.level.rawValue)
|
Text(verbatim: entry.level.rawValue)
|
||||||
.font(.caption.monospaced().bold())
|
.font(.caption.monospaced().bold())
|
||||||
.foregroundStyle(colorForLevel(entry.level))
|
.foregroundStyle(colorForLevel(entry.level))
|
||||||
.frame(width: 60, alignment: .leading)
|
.frame(width: 60, alignment: .leading)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ struct MCPServerDetailView: View {
|
|||||||
Text(key)
|
Text(key)
|
||||||
.font(.system(.caption, design: .monospaced))
|
.font(.system(.caption, design: .monospaced))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(String(repeating: "•", count: 10))
|
Text("••••••••••")
|
||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ struct MCPServerDetailView: View {
|
|||||||
Text(key)
|
Text(key)
|
||||||
.font(.system(.caption, design: .monospaced))
|
.font(.system(.caption, design: .monospaced))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(String(repeating: "•", count: 10))
|
Text("••••••••••")
|
||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ struct MCPServerPresetPickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(selectedPreset?.displayName ?? "Add from Preset")
|
(selectedPreset.map { Text(verbatim: $0.displayName) } ?? Text("Add from Preset"))
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text(selectedPreset?.description ?? "Pick an MCP server to add.")
|
(selectedPreset.map { Text(verbatim: $0.description) } ?? Text("Pick an MCP server to add."))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -83,14 +83,14 @@ struct MCPServerPresetPickerView: View {
|
|||||||
Image(systemName: preset.iconSystemName)
|
Image(systemName: preset.iconSystemName)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
Text(preset.displayName)
|
Text(verbatim: preset.displayName)
|
||||||
.font(.body.bold())
|
.font(.body.bold())
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemName: preset.transport == .http ? "network" : "terminal")
|
Image(systemName: preset.transport == .http ? "network" : "terminal")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Text(preset.description)
|
Text(verbatim: preset.description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(3)
|
.lineLimit(3)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ struct MCPServerTestResultView: View {
|
|||||||
Image(systemName: result.succeeded ? "checkmark.seal.fill" : "xmark.seal.fill")
|
Image(systemName: result.succeeded ? "checkmark.seal.fill" : "xmark.seal.fill")
|
||||||
.foregroundStyle(result.succeeded ? .green : .red)
|
.foregroundStyle(result.succeeded ? .green : .red)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(result.succeeded ? "Test passed" : "Test failed")
|
(result.succeeded ? Text("Test passed") : Text("Test failed"))
|
||||||
.font(.subheadline.bold())
|
.font(.subheadline.bold())
|
||||||
Text(String(format: "%.1fs · %d tools", result.elapsed, result.tools.count))
|
Text("\(result.elapsed.formatted(.number.precision(.fractionLength(1))))s · \(result.tools.count) tools")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,12 @@ struct MCPServerTestResultView: View {
|
|||||||
Button {
|
Button {
|
||||||
showOutput.toggle()
|
showOutput.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Label(showOutput ? "Hide Output" : "Show Output", systemImage: showOutput ? "chevron.up" : "chevron.down")
|
Label {
|
||||||
.font(.caption)
|
showOutput ? Text("Hide Output") : Text("Show Output")
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: showOutput ? "chevron.up" : "chevron.down")
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ struct MCPServersView: View {
|
|||||||
} else if let result = viewModel.testResults[server.name] {
|
} else if let result = viewModel.testResults[server.name] {
|
||||||
Image(systemName: result.succeeded ? "checkmark.circle.fill" : "xmark.circle.fill")
|
Image(systemName: result.succeeded ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||||
.foregroundStyle(result.succeeded ? .green : .red)
|
.foregroundStyle(result.succeeded ? .green : .red)
|
||||||
.help(result.succeeded ? "\(result.tools.count) tools" : "Test failed")
|
.help(result.succeeded ? Text("\(result.tools.count) tools") : Text("Test failed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ struct SignalSetupView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: viewModel.signalCLIInstalled ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
Image(systemName: viewModel.signalCLIInstalled ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
||||||
.foregroundStyle(viewModel.signalCLIInstalled ? .green : .orange)
|
.foregroundStyle(viewModel.signalCLIInstalled ? .green : .orange)
|
||||||
Text(viewModel.signalCLIInstalled ? "signal-cli is available on PATH" : "signal-cli not found on PATH — install it first")
|
(viewModel.signalCLIInstalled
|
||||||
|
? Text("signal-cli is available on PATH")
|
||||||
|
: Text("signal-cli not found on PATH — install it first"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(viewModel.signalCLIInstalled ? Color.primary : Color.orange)
|
.foregroundStyle(viewModel.signalCLIInstalled ? Color.primary : Color.orange)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ struct PlatformsView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: KnownPlatforms.icon(for: platform.name))
|
Image(systemName: KnownPlatforms.icon(for: platform.name))
|
||||||
.frame(width: 20)
|
.frame(width: 20)
|
||||||
Text(platform.displayName)
|
Text(verbatim: platform.displayName)
|
||||||
Spacer()
|
Spacer()
|
||||||
Circle()
|
Circle()
|
||||||
.fill(statusColor(viewModel.connectivity(for: platform)))
|
.fill(statusColor(viewModel.connectivity(for: platform)))
|
||||||
@@ -88,7 +88,7 @@ struct PlatformsView: View {
|
|||||||
Image(systemName: KnownPlatforms.icon(for: viewModel.selected.name))
|
Image(systemName: KnownPlatforms.icon(for: viewModel.selected.name))
|
||||||
.font(.title)
|
.font(.title)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(viewModel.selected.displayName)
|
Text(verbatim: viewModel.selected.displayName)
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
Text(statusDescription(viewModel.connectivity(for: viewModel.selected)))
|
Text(statusDescription(viewModel.connectivity(for: viewModel.selected)))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ struct ProfilesView: View {
|
|||||||
.font(.title)
|
.font(.title)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(profile.name).font(.title2.bold())
|
Text(profile.name).font(.title2.bold())
|
||||||
Text(profile.isActive ? "Active profile" : "Inactive")
|
(profile.isActive ? Text("Active profile") : Text("Inactive"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import SwiftUI
|
|||||||
private enum DashboardTab: String, CaseIterable {
|
private enum DashboardTab: String, CaseIterable {
|
||||||
case dashboard = "Dashboard"
|
case dashboard = "Dashboard"
|
||||||
case site = "Site"
|
case site = "Site"
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .dashboard: return "Dashboard"
|
||||||
|
case .site: return "Site"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProjectsView: View {
|
struct ProjectsView: View {
|
||||||
@@ -150,7 +157,7 @@ struct ProjectsView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: tab == .dashboard ? "square.grid.2x2" : "globe")
|
Image(systemName: tab == .dashboard ? "square.grid.2x2" : "globe")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
Text(tab.rawValue)
|
Text(tab.displayName)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ private struct QuickCommandEditor: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text(initial == nil ? "Add Quick Command" : "Edit /\(initial!.name)")
|
(initial == nil ? Text("Add Quick Command") : Text("Edit /\(initial!.name)"))
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Name (no leading slash)")
|
Text("Name (no leading slash)")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ConnectionStatusPill: View {
|
|||||||
Image(systemName: iconName)
|
Image(systemName: iconName)
|
||||||
.foregroundStyle(color)
|
.foregroundStyle(color)
|
||||||
.symbolRenderingMode(.hierarchical)
|
.symbolRenderingMode(.hierarchical)
|
||||||
Text(label)
|
labelText
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -39,7 +39,7 @@ struct ConnectionStatusPill: View {
|
|||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help(tooltip)
|
.help(tooltipText)
|
||||||
.popover(isPresented: $showDetails, arrowEdge: .bottom) {
|
.popover(isPresented: $showDetails, arrowEdge: .bottom) {
|
||||||
errorDetails.frame(width: 400)
|
errorDetails.frame(width: 400)
|
||||||
}
|
}
|
||||||
@@ -70,27 +70,27 @@ struct ConnectionStatusPill: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var label: String {
|
private var labelText: Text {
|
||||||
switch status.status {
|
switch status.status {
|
||||||
case .connected: return "Connected"
|
case .connected: return Text("Connected")
|
||||||
case .degraded: return "Connected — can't read Hermes state"
|
case .degraded: return Text("Connected — can't read Hermes state")
|
||||||
case .idle: return "Checking…"
|
case .idle: return Text("Checking…")
|
||||||
case .error(let message, _): return message
|
case .error(let message, _): return Text(verbatim: message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tooltip: String {
|
private var tooltipText: Text {
|
||||||
switch status.status {
|
switch status.status {
|
||||||
case .connected:
|
case .connected:
|
||||||
if let ts = status.lastSuccess {
|
if let ts = status.lastSuccess {
|
||||||
let fmt = RelativeDateTimeFormatter()
|
let fmt = RelativeDateTimeFormatter()
|
||||||
return "Last probe: \(fmt.localizedString(for: ts, relativeTo: Date()))"
|
return Text("Last probe: \(fmt.localizedString(for: ts, relativeTo: Date()))")
|
||||||
}
|
}
|
||||||
return "Connected"
|
return Text("Connected")
|
||||||
case .degraded(let reason):
|
case .degraded(let reason):
|
||||||
return "SSH works but \(reason). Click for diagnostics."
|
return Text("SSH works but \(reason). Click for diagnostics.")
|
||||||
case .idle: return "Waiting for first probe"
|
case .idle: return Text("Waiting for first probe")
|
||||||
case .error(_, _): return "Click for details"
|
case .error: return Text("Click for details")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ struct ManageServersView: View {
|
|||||||
Image(systemName: "server.rack")
|
Image(systemName: "server.rack")
|
||||||
.foregroundStyle(.blue)
|
.foregroundStyle(.blue)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(entry.displayName).font(.body)
|
Text(verbatim: entry.displayName).font(.body)
|
||||||
if case .ssh(let config) = entry.kind {
|
if case .ssh(let config) = entry.kind {
|
||||||
Text(summary(for: config))
|
Text(summary(for: config))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct ServerSwitcherToolbar: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(current.isRemote ? Color.blue : Color.green)
|
.fill(current.isRemote ? Color.blue : Color.green)
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
Text(current.displayName)
|
Text(verbatim: current.displayName)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Image(systemName: "chevron.down")
|
Image(systemName: "chevron.down")
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ struct ModelPickerSheet: View {
|
|||||||
.font(.system(.body, design: .default, weight: .medium))
|
.font(.system(.body, design: .default, weight: .medium))
|
||||||
Spacer()
|
Spacer()
|
||||||
if let ctx = model.contextDisplay {
|
if let ctx = model.contextDisplay {
|
||||||
Text(ctx + " ctx")
|
Text("\(ctx) ctx")
|
||||||
.font(.caption2.monospaced())
|
.font(.caption2.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ struct SkillsView: View {
|
|||||||
case hub = "Browse Hub"
|
case hub = "Browse Hub"
|
||||||
case updates = "Updates"
|
case updates = "Updates"
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: LocalizedStringResource {
|
||||||
|
switch self {
|
||||||
|
case .installed: return "Installed"
|
||||||
|
case .hub: return "Browse Hub"
|
||||||
|
case .updates: return "Updates"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -34,7 +42,7 @@ struct SkillsView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Picker("", selection: $currentTab) {
|
Picker("", selection: $currentTab) {
|
||||||
ForEach(Tab.allCases) { tab in
|
ForEach(Tab.allCases) { tab in
|
||||||
Text(tab.rawValue).tag(tab)
|
Text(tab.displayName).tag(tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ struct ToolsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: KnownPlatforms.icon(for: viewModel.selectedPlatform.name))
|
Image(systemName: KnownPlatforms.icon(for: viewModel.selectedPlatform.name))
|
||||||
Text(viewModel.selectedPlatform.displayName)
|
Text(verbatim: viewModel.selectedPlatform.displayName)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
statusDot(for: viewModel.connectivity[viewModel.selectedPlatform.name] ?? .notConfigured)
|
statusDot(for: viewModel.connectivity[viewModel.selectedPlatform.name] ?? .notConfigured)
|
||||||
Image(systemName: "chevron.down")
|
Image(systemName: "chevron.down")
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
},
|
},
|
||||||
"#%lld" : {
|
"#%lld" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"%@ ctx" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%@ in / %@ out" : {
|
"%@ in / %@ out" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -19,6 +22,9 @@
|
|||||||
},
|
},
|
||||||
"%@ reasoning" : {
|
"%@ reasoning" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"%@ tokens" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%@ · %@" : {
|
"%@ · %@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -40,6 +46,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%@s · %lld tools" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@s · %2$lld tools"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"%lld" : {
|
"%lld" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -117,6 +133,15 @@
|
|||||||
},
|
},
|
||||||
"22" : {
|
"22" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"30 Days" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"7 Days" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"90 Days" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"<%@>" : {
|
"<%@>" : {
|
||||||
|
|
||||||
@@ -132,6 +157,9 @@
|
|||||||
},
|
},
|
||||||
"Actions" : {
|
"Actions" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Active" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Active profile" : {
|
"Active profile" : {
|
||||||
|
|
||||||
@@ -186,12 +214,21 @@
|
|||||||
},
|
},
|
||||||
"After approving in your browser, the provider shows a code. Paste it below and submit." : {
|
"After approving in your browser, the provider shows a code. Paste it below and submit." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Agent" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"All" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"All Levels" : {
|
"All Levels" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"All Sessions" : {
|
"All Sessions" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"All Time" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"All installed hub skills are up to date." : {
|
"All installed hub skills are up to date." : {
|
||||||
|
|
||||||
@@ -243,18 +280,27 @@
|
|||||||
},
|
},
|
||||||
"Browse" : {
|
"Browse" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Browse Hub" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Browse the Hub" : {
|
"Browse the Hub" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Browse..." : {
|
"Browse..." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Browser" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"By Day" : {
|
"By Day" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"By Hour" : {
|
"By Hour" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"CLI" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Call timeout" : {
|
"Call timeout" : {
|
||||||
|
|
||||||
@@ -285,6 +331,9 @@
|
|||||||
},
|
},
|
||||||
"Check for Updates…" : {
|
"Check for Updates…" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Checking…" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Choose a cron job from the list" : {
|
"Choose a cron job from the list" : {
|
||||||
|
|
||||||
@@ -315,6 +364,9 @@
|
|||||||
},
|
},
|
||||||
"Click Add to connect to a remote Hermes installation over SSH." : {
|
"Click Add to connect to a remote Hermes installation over SSH." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Click for details" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Clicking Start OAuth opens the provider's authorization page in your browser. After you approve, copy the code the provider displays and paste it back into the terminal that appears next." : {
|
"Clicking Start OAuth opens the provider's authorization page in your browser. After you approve, copy the code the provider displays and paste it back into the terminal that appears next." : {
|
||||||
|
|
||||||
@@ -357,6 +409,9 @@
|
|||||||
},
|
},
|
||||||
"Connected" : {
|
"Connected" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Connected — can't read Hermes state" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Connection" : {
|
"Connection" : {
|
||||||
|
|
||||||
@@ -420,6 +475,9 @@
|
|||||||
},
|
},
|
||||||
"Credentials" : {
|
"Credentials" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Cron" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Cron Jobs" : {
|
"Cron Jobs" : {
|
||||||
|
|
||||||
@@ -528,9 +586,15 @@
|
|||||||
},
|
},
|
||||||
"Error" : {
|
"Error" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Errors" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Exclude" : {
|
"Exclude" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Execute" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Expected at %@" : {
|
"Expected at %@" : {
|
||||||
|
|
||||||
@@ -552,6 +616,9 @@
|
|||||||
},
|
},
|
||||||
"Feishu Setup Docs" : {
|
"Feishu Setup Docs" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Fetch" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Files" : {
|
"Files" : {
|
||||||
|
|
||||||
@@ -663,6 +730,9 @@
|
|||||||
},
|
},
|
||||||
"Install signal-cli" : {
|
"Install signal-cli" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Installed" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Interact" : {
|
"Interact" : {
|
||||||
|
|
||||||
@@ -681,6 +751,9 @@
|
|||||||
},
|
},
|
||||||
"Last Output" : {
|
"Last Output" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Last probe: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Last run: %@" : {
|
"Last run: %@" : {
|
||||||
|
|
||||||
@@ -939,6 +1012,9 @@
|
|||||||
},
|
},
|
||||||
"Optionally focus the summary on a specific topic. Leave blank to compress evenly." : {
|
"Optionally focus the summary on a specific topic. Leave blank to compress evenly." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Other" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Output" : {
|
"Output" : {
|
||||||
|
|
||||||
@@ -975,6 +1051,9 @@
|
|||||||
},
|
},
|
||||||
"Personalities" : {
|
"Personalities" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Pick an MCP server to add." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Pick one from the list, or add a new server from the toolbar." : {
|
"Pick one from the list, or add a new server from the toolbar." : {
|
||||||
|
|
||||||
@@ -1044,6 +1123,9 @@
|
|||||||
},
|
},
|
||||||
"Re-run" : {
|
"Re-run" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Read" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Reasoning" : {
|
"Reasoning" : {
|
||||||
|
|
||||||
@@ -1054,7 +1136,7 @@
|
|||||||
"Reconnect" : {
|
"Reconnect" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Recording..." : {
|
"Recording…" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Refresh" : {
|
"Refresh" : {
|
||||||
@@ -1185,6 +1267,9 @@
|
|||||||
},
|
},
|
||||||
"SOUL.md describes the agent's voice, values, and personality at ~/.hermes/SOUL.md. It is injected into every session's context." : {
|
"SOUL.md describes the agent's voice, values, and personality at ~/.hermes/SOUL.md. It is injected into every session's context." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"SSH works but %@. Click for diagnostics." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Save" : {
|
"Save" : {
|
||||||
|
|
||||||
@@ -1314,6 +1399,9 @@
|
|||||||
},
|
},
|
||||||
"Signal integration requires signal-cli (Java-based) installed locally. Link this Mac as a Signal device, then keep the daemon running so hermes can send/receive messages." : {
|
"Signal integration requires signal-cli (Java-based) installed locally. Link this Mac as a Signal device, then keep the daemon running so hermes can send/receive messages." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Site" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Skills" : {
|
"Skills" : {
|
||||||
|
|
||||||
@@ -1498,6 +1586,9 @@
|
|||||||
},
|
},
|
||||||
"Updated: %@" : {
|
"Updated: %@" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Updates" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Upload" : {
|
"Upload" : {
|
||||||
|
|
||||||
@@ -1537,6 +1628,9 @@
|
|||||||
},
|
},
|
||||||
"Waiting for authorization URL…" : {
|
"Waiting for authorization URL…" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Waiting for first probe" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Waiting for hermes to prompt for the code…" : {
|
"Waiting for hermes to prompt for the code…" : {
|
||||||
|
|
||||||
@@ -1684,6 +1778,9 @@
|
|||||||
},
|
},
|
||||||
"•" : {
|
"•" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"••••••••••" : {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user