@@ -319,22 +319,45 @@ struct ChatView: View {
319319 }
320320
321321 private var composer: some View {
322− HStack(spacing: 8) {
323− TextField("Message", text: $draft, axis: .vertical)
324− .textFieldStyle(.roundedBorder)
325− .lineLimit(1...4)
326− .disabled(core.view.activePeerBlocked)
327− Button {
328− let text = draft.trimmingCharacters(in: .whitespacesAndNewlines)
329− guard !text.isEmpty else { return }
330− core.update(.composeChanged(text))
331− core.update(.sendText)
332− draft = ""
333− } label: {
334− Image(systemName: "arrow.up.circle.fill").font(.title)
322+ VStack(spacing: 0) {
323+ // Errors from send_text ("message too long", "contact list is full",
324+ // "gave up sending …", "invalid key") are set on model.error and rendered
325+ // into view.error, but without this the chat page swallowed them — the
326+ // user tapped send, it failed, and they saw nothing.
327+ if !core.view.error.isEmpty {
328+ Text(core.view.error)
329+ .font(.caption)
330+ .foregroundColor(.red)
331+ .frame(maxWidth: .infinity, alignment: .leading)
332+ .padding(.horizontal)
333+ .padding(.top, 4)
334+ }
335+ HStack(spacing: 8) {
336+ TextField("Message", text: $draft, axis: .vertical)
337+ .textFieldStyle(.roundedBorder)
338+ .lineLimit(1...4)
339+ .disabled(core.view.activePeerBlocked)
340+ Button {
341+ let text = draft.trimmingCharacters(in: .whitespacesAndNewlines)
342+ guard !text.isEmpty else { return }
343+ core.update(.composeChanged(text))
344+ core.update(.sendText)
345+ // Only clear the local draft when the core actually accepted the
346+ // message. send_text refuses (and sets error) on a blocked peer,
347+ // an oversized body, or a full contact list — and it preserves
348+ // model.compose on those paths so the user's text is not lost.
349+ // Clearing draft unconditionally wiped it from the UI even though
350+ // the core kept it, so the failure was both invisible and
351+ // destructive: the text was gone with no explanation.
352+ if core.view.error.isEmpty {
353+ draft = ""
354+ }
355+ } label: {
356+ Image(systemName: "arrow.up.circle.fill").font(.title)
357+ }
358+ .disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
359+ || core.view.activePeerBlocked)
335360 }
336− .disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
337− || core.view.activePeerBlocked)
338361 }
339362 .padding(.horizontal).padding(.vertical, 8)
340363 .background(.bar)