mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
Part 1 — Simplified Chinese localization of the iOS app layer: - ios/Hapi/Resources/Localizable.xcstrings: hand-authored String Catalog (395 keys, zh-Hans; en implicit as source). Terminology mined from web/src/lib/locales/zh-CN.ts (会话/新建会话/权限模式/允许/拒绝/工作树/ 机器/语音输入/用量/草稿夹 …). - Mechanical edits only: merged multi-part string concatenations into single LocalizedStringKey literals, converted ternary/plain-String user copy to String(localized:), switched helper params (detailRow, DashboardCard, optionPicker, notice) to LocalizedStringKey. - LocalizedNoticeMapper (app layer): display-point translation of the 27 known HapiKit-emitted strings (ChatInteractor notices, dictation errors, window-sync warnings, files fallbacks, worktree-name validation) with verbatim passthrough for server-originated text; package stays language-free. - Deliberately untranslated (web parity / non-copy): event rows (EventPresentation — web renders presentation.ts verbatim), catalog option labels (Default/Auto/Sonnet/permission modes — web shows them verbatim), tool names, code-like titles (grep(pattern:), MCP:, Skill, Task), unit suffixes (B/KB/MB, m/h/d/w), decorative separators. - Language row wired: AppLanguage gains .system (follow system, new default); explicit picks write the AppleLanguages override, Follow system removes it; footer notes a relaunch applies it (no supported in-place SwiftUI locale swap). - project.pbxproj: zh-Hans added to knownRegions. Part 2 — #39 catalog dedupe (HapiProtocol): - NewSessionCatalogs.claudeModels/claudeEfforts now derive from ClaudeModels/ClaudeEfforts (single source shared with ModelCatalog); codexReasoningEfforts stays own data (web CODEX_REASONING_EFFORT_OPTIONS minus max); effortLabel delegates to ModelCatalog.capitalizedFirst. - New CatalogTests lock test for the derived option lists. Gate: bash ios/scripts/linux-test.sh — 459 tests green.
146 lines
5.1 KiB
Swift
146 lines
5.1 KiB
Swift
import HapiClient
|
|
import HapiProtocol
|
|
import SwiftUI
|
|
|
|
/// Session config sheet (A-M3b): permission mode / model / effort sections,
|
|
/// catalog-driven per flavor — the SwiftUI twin of the Android
|
|
/// `SessionConfigSheet`. Pickers apply optimistically through the interactor
|
|
/// (`setPermissionMode` & co., server truth reloaded on error); a flavor
|
|
/// without a known model catalog simply hides that section.
|
|
struct SessionConfigView: View {
|
|
let interactor: ChatInteractor
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
let config = interactor.config
|
|
NavigationStack {
|
|
List {
|
|
if !config.active {
|
|
notice("Session is offline — changes apply after it resumes or may be rejected.")
|
|
} else if config.controlledByUser {
|
|
notice("Session is controlled from the terminal — config changes will be rejected.")
|
|
}
|
|
|
|
if !config.permissionModes.isEmpty {
|
|
Section("Permission mode") {
|
|
ForEach(config.permissionModes, id: \.mode) { option in
|
|
OptionRow(
|
|
label: option.label,
|
|
selected: option.mode == (config.permissionMode ?? .default),
|
|
tone: option.tone
|
|
) {
|
|
interactor.setPermissionMode(option.mode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if config.modelOptions != nil || config.modelOptionsLoading {
|
|
Section("Model") {
|
|
modelSection(config)
|
|
}
|
|
}
|
|
|
|
if let effortOptions = config.effortOptions {
|
|
Section("Effort") {
|
|
let current = normalizedCurrentEffort(config)
|
|
ForEach(effortOptions, id: \.self) { option in
|
|
OptionRow(label: option.label, selected: option.value == current) {
|
|
interactor.setEffort(option.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Session Settings")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Done") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.presentationDetents([.medium, .large])
|
|
.onAppear {
|
|
interactor.loadModelOptions()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func modelSection(_ config: SessionConfigState) -> some View {
|
|
if config.modelOptionsLoading {
|
|
HStack(spacing: 8) {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
Text("Loading models…")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else if config.modelOptions?.isEmpty != false {
|
|
Text("Model list unavailable for this session.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
let current = normalizedCurrentModel(config)
|
|
ForEach(config.modelOptions ?? [], id: \.self) { option in
|
|
OptionRow(label: option.label, selected: option.value == current) {
|
|
interactor.setModel(option.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Claude models normalize `auto`/`default` to the nil Default row.
|
|
private func normalizedCurrentModel(_ config: SessionConfigState) -> String? {
|
|
config.flavor == "claude" ? ModelCatalog.normalizeClaudeModel(config.model) : config.model
|
|
}
|
|
|
|
private func normalizedCurrentEffort(_ config: SessionConfigState) -> String? {
|
|
config.flavor == "claude" ? ModelCatalog.normalizeClaudeEffort(config.effort) : config.effort
|
|
}
|
|
|
|
private func notice(_ text: LocalizedStringKey) -> some View {
|
|
Section {
|
|
Label(text, systemImage: "info.circle")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One catalog row: label + trailing checkmark, tinted by the mode's tone.
|
|
private struct OptionRow: View {
|
|
let label: String
|
|
let selected: Bool
|
|
var tone: PermissionModeTone = .neutral
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack {
|
|
Text(label)
|
|
.foregroundStyle(toneColor)
|
|
Spacer()
|
|
if selected {
|
|
Image(systemName: "checkmark")
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(.tint)
|
|
}
|
|
}
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private var toneColor: Color {
|
|
switch tone {
|
|
case .danger: .red
|
|
case .warning: .orange
|
|
case .neutral, .info: .primary
|
|
}
|
|
}
|
|
}
|