-
Notifications
You must be signed in to change notification settings - Fork 552
iOS: add tasks, transcript search, export, reactions, and branches #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ struct ChatListView: View { | |
| /// cannot push without a tap, and a new bot appearing silently at the | ||
| /// bottom of the roster is a poor answer to pressing +. | ||
| @State private var path = NavigationPath() | ||
| @State private var searchHits: [SearchHit] = [] | ||
| @State private var searching = false | ||
|
|
||
| var body: some View { | ||
| NavigationStack(path: $path) { | ||
|
|
@@ -40,6 +42,29 @@ struct ChatListView: View { | |
| } | ||
| } | ||
|
|
||
| if !query.isEmpty, !searchHits.isEmpty { | ||
| HStack { | ||
| Text("Messages") | ||
| .font(.system(size: 13, weight: .semibold)) | ||
| .foregroundStyle(Color.secondary) | ||
| Spacer() | ||
| if searching { ProgressView().controlSize(.small) } | ||
| } | ||
| .padding(.top, 10) | ||
| .padding(.bottom, 4) | ||
|
|
||
| ForEach(searchHits) { hit in | ||
| Button { | ||
| Task { | ||
| if let chat = await session.open(hit) { path.append(chat) } | ||
| } | ||
| } label: { | ||
| SearchHitRow(hit: hit) | ||
| } | ||
| .buttonStyle(.plain) | ||
| } | ||
| } | ||
|
|
||
| ForEach(chats) { summary in | ||
| NavigationLink(value: summary.chat) { | ||
| ChatRow( | ||
|
|
@@ -56,7 +81,7 @@ struct ChatListView: View { | |
| } | ||
| .refreshable { await session.refresh() } | ||
| .overlay { | ||
| if chats.isEmpty { | ||
| if chats.isEmpty && searchHits.isEmpty { | ||
| ContentUnavailableView( | ||
| query.isEmpty ? "No bots yet" : "Nothing matches", | ||
| systemImage: query.isEmpty ? "bubble.left.and.bubble.right" : "magnifyingglass", | ||
|
|
@@ -73,6 +98,19 @@ struct ChatListView: View { | |
| .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) | ||
| .toolbar(.hidden, for: .navigationBar) | ||
| .navigationDestination(for: Chat.self) { ChatView(chat: $0) } | ||
| .task(id: query) { | ||
| let expected = query | ||
| guard expected.trimmingCharacters(in: .whitespacesAndNewlines).count >= 2 else { | ||
| searchHits = [] | ||
| searching = false | ||
| return | ||
| } | ||
| searching = true | ||
| try? await Task.sleep(for: .milliseconds(250)) | ||
| guard !Task.isCancelled, query == expected else { return } | ||
| searchHits = await session.search(expected) | ||
| searching = false | ||
|
Comment on lines
+101
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline ios/App/ChatListView.swift --items all
rg -n -C 5 'Task\.isCancelled|session\.search|searchHits =' ios/App/ChatListView.swiftRepository: milind-soni/OpenMausBot Length of output: 1267 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ChatListView state and task context ---'
sed -n '1,125p' ios/App/ChatListView.swift
printf '%s\n' '--- search implementations and call sites ---'
rg -n -C 8 'func search|async .*search|session\.search|searchHits|searching' --glob '*.swift' .Repository: milind-soni/OpenMausBot Length of output: 15671 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Session isolation and search implementation ---'
sed -n '1,70p' ios/App/Session.swift
sed -n '335,375p' ios/App/Session.swift
printf '%s\n' '--- Client request and cancellation behavior ---'
sed -n '1,90p' ios/Sources/CompanionCore/Client.swift
rg -n -C 12 'func send|withChecked|URLSession|CancellationError|isCancelled' ios/Sources/CompanionCore ios --glob '*.swift'Repository: milind-soni/OpenMausBot Length of output: 50379 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
view = Path("ios/App/ChatListView.swift").read_text()
session = Path("ios/App/Session.swift").read_text()
client = Path("ios/Sources/CompanionCore/Client.swift").read_text()
task = view[view.index(".task(id: query)"):view.index("\n }\n }", view.index(".task(id: query)"))]
search = session[session.index("func search(_ query: String)"):session.index("\n /// Resolve", session.index("func search(_ query: String)"))]
perform = client[client.index("private func perform(_ request: URLRequest)"):client.index("\n }\n\n // MARK: - Pairing", client.index("private func perform(_ request: URLRequest)"))]
assert "guard !Task.isCancelled, query == expected else { return }" in task
assert "searchHits = await session.search(expected)" in task
assert "guard !Task.isCancelled, query == expected else { return }\n searchHits =" not in task
assert "catch {" in search and "return []" in search
assert "catch {" in perform and "throw APIError.transport" in perform
print("stale-result path is present:")
print("- the task performs an awaited search without a post-await identity/cancellation guard")
print("- Session.search converts request errors, including cancellation-related errors, into []")
print("- Client.perform wraps URLSession errors before Session.search catches them")Repository: milind-soni/OpenMausBot Length of output: 337 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
view = Path("ios/App/ChatListView.swift").read_text()
session = Path("ios/App/Session.swift").read_text()
client = Path("ios/Sources/CompanionCore/Client.swift").read_text()
start = view.index(".task(id: query)")
task = view[start:view.index("\n }\n }", start)]
search_start = session.index("func search(_ query: String)")
search = session[search_start:session.index("\n /// Resolve", search_start)]
perform_start = client.index("private func perform(_ request: URLRequest)")
perform = client[perform_start:client.index("\n }\n\n // MARK: - Pairing", perform_start)]
checks = {
"pre-await guard": "guard !Task.isCancelled, query == expected else { return }" in task,
"awaited search assignment": "searchHits = await session.search(expected)" in task,
"post-await guard": task.find("let results = await session.search(expected)") >= 0,
"Session catches search errors": "catch {" in search and "return []" in search,
"Client wraps URLSession errors": "catch {" in perform and "throw APIError.transport" in perform,
}
for name, passed in checks.items():
print(f"{name}: {'yes' if passed else 'no'}")
assert checks["pre-await guard"]
assert checks["awaited search assignment"]
assert not checks["post-await guard"]
assert checks["Session catches search errors"]
assert checks["Client wraps URLSession errors"]
print("stale-result path is present")
PYRepository: milind-soni/OpenMausBot Length of output: 334 Discard results from canceled searches. If 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -144,6 +182,39 @@ struct ChatListView: View { | |
| } | ||
| } | ||
|
|
||
| struct SearchHitRow: View { | ||
| let hit: SearchHit | ||
|
|
||
| var body: some View { | ||
| HStack(alignment: .top, spacing: 12) { | ||
| Image(systemName: hit.role == .user ? "person.fill" : "bubble.left.fill") | ||
| .foregroundStyle(Color.secondary) | ||
| .frame(width: 26, height: 26) | ||
| .background(Circle().fill(Color.secondary.opacity(0.13))) | ||
|
|
||
| VStack(alignment: .leading, spacing: 4) { | ||
| HStack { | ||
| Text(hit.name).font(.system(size: 15, weight: .semibold)) | ||
| if let task = hit.task, !task.isEmpty { | ||
| Text(task).font(.system(size: 12)).foregroundStyle(Color.secondary) | ||
| } | ||
| Spacer() | ||
| Text(RelativeStamp.list(hit.at)) | ||
| .font(.system(size: 12)) | ||
| .foregroundStyle(Color.secondary) | ||
| } | ||
| Text(hit.snippet) | ||
| .font(.system(size: 14)) | ||
| .foregroundStyle(Color.secondary) | ||
| .lineLimit(2) | ||
| .multilineTextAlignment(.leading) | ||
| } | ||
| } | ||
| .padding(.vertical, 10) | ||
| .contentShape(Rectangle()) | ||
| } | ||
| } | ||
|
|
||
| struct ChatRow: View { | ||
| let chat: Chat | ||
| let preview: String | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the roadmap milestone target.
This document describes the iOS companion roadmap, but the milestone is named “Desktop conversation parity.” Rename it to “iOS conversation parity with desktop” or “Conversation parity” so readers do not interpret these features as desktop-only.
🤖 Prompt for AI Agents