Fix phone keyboard and touch targets, render notes as Markdown
This commit is contained in:
@@ -12,6 +12,8 @@ Left out on the phone: the Server tab and running byre locally, the shell, file,
|
||||
|
||||
`mac/phone.sh --simulator` builds an ad-hoc-signed app for the iOS simulator instead, at `mac/.build/phone/iphonesimulator/Byre.app`; install it with `xcrun simctl install <device> <app>` and start it with `xcrun simctl launch <device> run.fc.byre.ios`. The simulator ignores entitlements given to `codesign` and refuses to launch an ad-hoc app signed with restricted ones, so the script links `mac/phone/byre.entitlements` into the binary's `__TEXT,__entitlements` section, as Xcode does; without the iCloud entitlement CloudKit raises at launch. Its node signs in to Tailscale like any other: `xcrun simctl launch --console-pty` prints libtailscale's sign-in URL, which changes on every launch.
|
||||
|
||||
The simulator's window (Xcode's Device Hub) lists the app's own accessibility elements, so hands' `ax_tree` and `act` `press` drive the app without a mouse. Hands' mouse steps (`click`, `drag`, `scroll`) refuse that window, because Device Hub reports no process id; a gesture such as dragging the keyboard away needs mouse events posted with `CGEvent` instead. `xcrun simctl io booted screenshot --display LCD <file>` captures the app at full resolution.
|
||||
|
||||
## Platform facts
|
||||
|
||||
- CloudKit on iOS requires the `remote-notification` background mode in `Info.plist`, even though the app sets up no push of its own.
|
||||
|
||||
@@ -439,7 +439,7 @@ private struct WebButton: View {
|
||||
.font(Fonts.font(13, weight: primary ? 700 : 400))
|
||||
.foregroundStyle(primary ? Palette.bg : danger ? Palette.danger : Palette.text)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.vertical, touchOnly ? 7 : 4)
|
||||
.background(primary ? fill : Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(primary ? fill : hover && enabled ? Palette.muted : Palette.border))
|
||||
.opacity(enabled ? (configuration.isPressed ? 0.8 : 1) : 0.45)
|
||||
@@ -458,7 +458,9 @@ struct SmallLinkButtonStyle: ButtonStyle {
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, touchOnly ? 3 : 0)
|
||||
.overlay(RoundedRectangle(cornerRadius: 4).stroke(border))
|
||||
.padding(.vertical, touchOnly ? 5 : 0)
|
||||
.opacity(enabled ? 1 : 0.45)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@@ -124,11 +124,9 @@ private struct NoteRow: View {
|
||||
@ViewState private var frame: CGRect = .zero
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
Text(note.text)
|
||||
.textSelection(.enabled)
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
MarkdownView(blocks: parseMarkdown(note.text))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Button("Delete") {
|
||||
Task {
|
||||
if !(await overlays.showConfirm(["Delete this note? Your lead stops applying it.", note.text], action: "Delete", danger: true, anchor: frame)) {
|
||||
|
||||
@@ -56,6 +56,7 @@ struct PhoneView: View {
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var tab = PhoneTab.lead
|
||||
@ViewState private var path: [PhoneRoute] = []
|
||||
@ViewState private var typing = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -77,36 +78,17 @@ struct PhoneView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
HStack(spacing: 4) {
|
||||
tabButton("Lead", .lead)
|
||||
HStack(spacing: 0) {
|
||||
tabButton("Tasks", .tasks)
|
||||
if !store.inbox.isEmpty {
|
||||
Badge(count: store.inbox.count)
|
||||
}
|
||||
}
|
||||
tabButton("More", .more)
|
||||
Spacer(minLength: 0)
|
||||
ConnectionMark()
|
||||
Button {
|
||||
tab = .tasks
|
||||
path = []
|
||||
} label: {
|
||||
Image(systemName: "list.bullet")
|
||||
.font(Fonts.font(17, weight: 600))
|
||||
.foregroundStyle(Palette.accent)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Task list")
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
if !typing {
|
||||
tabBar
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||||
typing = true
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
typing = false
|
||||
}
|
||||
.overlay {
|
||||
OverlayHost()
|
||||
}
|
||||
@@ -159,6 +141,39 @@ struct PhoneView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var tabBar: some View {
|
||||
HStack(spacing: 4) {
|
||||
tabButton("Lead", .lead)
|
||||
HStack(spacing: 0) {
|
||||
tabButton("Tasks", .tasks)
|
||||
if !store.inbox.isEmpty {
|
||||
Badge(count: store.inbox.count)
|
||||
}
|
||||
}
|
||||
tabButton("More", .more)
|
||||
Spacer(minLength: 0)
|
||||
ConnectionMark()
|
||||
Button {
|
||||
tab = .tasks
|
||||
path = []
|
||||
} label: {
|
||||
Image(systemName: "list.bullet")
|
||||
.font(Fonts.font(17, weight: 600))
|
||||
.foregroundStyle(Palette.accent)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Task list")
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
}
|
||||
}
|
||||
|
||||
private func shown(_ which: PhoneTab, @ViewBuilder _ content: () -> some View) -> some View {
|
||||
content()
|
||||
.opacity(tab == which ? 1 : 0)
|
||||
|
||||
@@ -83,7 +83,7 @@ struct ChecksView: View {
|
||||
case "required":
|
||||
return "Required"
|
||||
case "agent":
|
||||
return "One of claude, codex and agy required"
|
||||
return "One of claude, codex or agy required"
|
||||
default:
|
||||
return "Optional"
|
||||
}
|
||||
|
||||
@@ -303,7 +303,9 @@ final class SwitcherModel {
|
||||
if store.inbox.contains(where: { $0.taskId == taskId }) {
|
||||
store.showQuestion(taskId)
|
||||
} else {
|
||||
store.composerFocus = taskId
|
||||
if !touchOnly {
|
||||
store.composerFocus = taskId
|
||||
}
|
||||
store.focusTask(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,7 +1054,11 @@ private func toolTarget(_ input: (any Sendable)?, _ root: String) -> String {
|
||||
guard let path = (i?["file_path"] ?? i?["path"]) as? String else {
|
||||
return ""
|
||||
}
|
||||
return relative(path, root)
|
||||
let short = relative(path, root)
|
||||
if short != path || !path.hasPrefix("/") {
|
||||
return short
|
||||
}
|
||||
return URL(fileURLWithPath: path).lastPathComponent
|
||||
}
|
||||
|
||||
private func relative(_ path: String, _ root: String) -> String {
|
||||
|
||||
Reference in New Issue
Block a user