Merge nucleic/warm-quartz-marten-mswp into dev

This commit is contained in:
2026-08-03 23:56:01 -07:00
parent 46eb3ccdbe
commit f80b1952c3
3 changed files with 40 additions and 22 deletions
@@ -6,18 +6,16 @@ import NucleicProtocol
/// The division of labor with the Mac is deliberate and worth stating once here, because it's /// The division of labor with the Mac is deliberate and worth stating once here, because it's
/// what makes a rail on a thin remote client honest: /// what makes a rail on a thin remote client honest:
/// ///
/// - **The host routes.** The matrix, the connected providers, the Settings pin, live quota and /// - **The host publishes the routes.** The matrix, connected providers, Settings pin, live quota,
/// provider incidents live on the Mac. A send carries the rail's *stop*, never a concrete pair /// and provider incidents live on the Mac. The phone only chooses among the concrete routes in
/// (`StartChatRequest.intelligence`, `ClientMsg.setSessionIntelligence`), and the host resolves /// that projection; it does not recreate the matrix.
/// it with its own classifier. So the phone can never pin a worse model than the Mac would.
/// - **The phone previews.** It runs the shared `HeuristicPurposeClassifier` — the same code the /// - **The phone previews.** It runs the shared `HeuristicPurposeClassifier` — the same code the
/// host runs, moved into NucleicProtocol precisely so both sides classify identically — over /// host runs, moved into NucleicProtocol precisely so both sides classify identically — over
/// the draft, and looks the answer up in the host's projected route table. Sub-millisecond, no /// the draft, and looks the answer up in the host's projected route table. Sub-millisecond, no
/// round trip, so the route line can follow every keystroke on a cellular link. /// round trip, so the route line can follow every keystroke on a cellular link.
/// - **The preview can be wrong, and says so by being a preview.** The host's send-time /// - **The displayed pair is the launch authority.** A new-chat send carries the stop *and* the
/// classification runs a fuller stack; when the two disagree, the transcript's routing note /// projected model/effort pair. The host preserves that explicit pair rather than classifying a
/// records what actually ran. That's the same relationship the Mac's own pre-send preview has /// second time, so the session cannot start on a model the route line never showed.
/// with its send-time resolve.
extension RemoteStore { extension RemoteStore {
/// Whether the context host offers the rail at all. False keeps the composers on their manual /// Whether the context host offers the rail at all. False keeps the composers on their manual
@@ -2741,10 +2741,10 @@ final class RemoteStore: ObservableObject {
// MARK: - Control intents (control scope; the same actions the Mac can take) // MARK: - Control intents (control scope; the same actions the Mac can take)
/// Start a chat on the owning Mac. `intelligence` is the rail's stop: pass it and leave /// Start a chat on the owning Mac. The routed composer passes `intelligence` together with
/// `model` nil to have the host classify the opening message and route it (the composer's /// the concrete model/effort displayed from the host's projected route table: the stop keeps
/// normal path); pass a concrete `model`/`effort` instead when the user pinned one. Sending /// the rail positioned, while the explicit pair guarantees the session runs what was shown.
/// both leaves the explicit pair in charge — see `AppStore.remoteStartSelection`. /// Older clients may still pass only the stop and let the host resolve it.
func startChat(in projectID: ProjectID, message: String, model: String? = nil, func startChat(in projectID: ProjectID, message: String, model: String? = nil,
effort: String? = nil, baseBranch: String? = nil, effort: String? = nil, baseBranch: String? = nil,
useWorktree: Bool = true, auto: Bool? = nil, useWorktree: Bool = true, auto: Bool? = nil,
@@ -287,7 +287,7 @@ struct StartChatComposer: View {
orchestraAvailable: store.intelligenceCatalog.orchestraAvailable, orchestraAvailable: store.intelligenceCatalog.orchestraAvailable,
composerText: draft, composerText: draft,
routeDescription: store.intelligenceRouteDescription( routeDescription: store.intelligenceRouteDescription(
for: draft, level: previewLevel), for: routingPrompt, level: previewLevel),
unavailableReason: store.canControl ? nil : "This device is view-only.", unavailableReason: store.canControl ? nil : "This device is view-only.",
onPreviewChanged: { intelligencePreview = $0 }) onPreviewChanged: { intelligencePreview = $0 })
IntelligenceRouteLine( IntelligenceRouteLine(
@@ -317,27 +317,47 @@ struct StartChatComposer: View {
private var previewLevel: Int { intelligencePreview ?? intelligence } private var previewLevel: Int { intelligencePreview ?? intelligence }
private var previewRoute: WireIntelligenceCatalog.Route? { private var previewRoute: WireIntelligenceCatalog.Route? {
store.intelligenceRoute(for: draft, level: previewLevel) store.intelligenceRoute(for: routingPrompt, level: previewLevel)
}
/// The route at the committed stop, distinct from the under-finger preview. This exact pair is
/// copied into the start request so the host cannot classify again and run something other
/// than the model and effort named by the route line.
private var committedRoute: WireIntelligenceCatalog.Route? {
store.intelligenceRoute(for: routingPrompt, level: intelligence)
}
/// Attachment-only starts still need a visible concrete route. The host ultimately adds file
/// references to the opening prompt; filenames give the shared classifier equivalent context
/// before launch and, critically, are used by both the route line and the committed request.
private var routingPrompt: String {
let text = draft.trimmingCharacters(in: .whitespacesAndNewlines)
guard text.isEmpty, !attachments.isEmpty else { return draft }
return "Work with the attached files: " + attachments.map(\.filename).joined(separator: ", ")
} }
private var orchestraActive: Bool { orchestra || MobileEfforts.isOrchestra(effort) } private var orchestraActive: Bool { orchestra || MobileEfforts.isOrchestra(effort) }
private var canStart: Bool { private var canStart: Bool {
selected != nil && store.canControl guard selected != nil, store.canControl,
&& !(draft.trimmingCharacters(in: .whitespaces).isEmpty && attachments.isEmpty) !(draft.trimmingCharacters(in: .whitespaces).isEmpty && attachments.isEmpty)
else { return false }
guard store.routesIntelligence, !orchestra else { return true }
return committedRoute?.isAvailable == true
} }
/// Start the chat. When the rail is driving, the request carries the **stop** and no model: /// Start the chat. When the rail is driving, the request carries both its stop and the exact
/// the host classifies the real prompt with its own stack and picks the pair, so the phone /// concrete pair shown on screen. The stop preserves the rail position; the pair is the launch
/// never pins a worse model than the Mac would have (see `RemoteIntelligence`). A host that /// authority, preventing a second host-side classification from contradicting the route line.
/// doesn't route sends the concrete values from its manual menus instead.
private func start() { private func start() {
guard let project = selected else { return } guard let project = selected else { return }
let routed = store.routesIntelligence && !orchestra let routed = store.routesIntelligence && !orchestra
let sentModel = routed ? nil : model let route = routed ? committedRoute : nil
guard !routed || route?.isAvailable == true else { return }
let sentModel = routed ? route?.model : model
let sentEffort = orchestra let sentEffort = orchestra
? store.modelCatalog.orchestraSentinelOrFallback ? store.modelCatalog.orchestraSentinelOrFallback
: (routed ? nil : effort) : (routed ? route?.effort : effort)
let sentLevel = store.routesIntelligence ? intelligence : nil let sentLevel = store.routesIntelligence ? intelligence : nil
if runOnMesh && meshAvailable { if runOnMesh && meshAvailable {