Fix phone keyboard and touch targets, render notes as Markdown

This commit is contained in:
Ian Gulliver
2026-10-22 06:33:03 -07:00
parent e2d747d7d3
commit 188a458930
7 changed files with 59 additions and 36 deletions
+2
View File
@@ -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.
+3 -1
View File
@@ -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())
}
+2 -4
View File
@@ -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)) {
+43 -28
View File
@@ -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)
+1 -1
View File
@@ -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"
}
+3 -1
View File
@@ -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)
}
}
+5 -1
View File
@@ -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 {