Skip to content

Commit 52ef272

Browse files
committed
feat(chat): queue in-flight sends and batch queued drafts
Keep user intent while a response is running by showing queued messages and dispatching them as one payload when possible. Add Send now, retry-safe queue draining, and docs/tests to lock behavior.
1 parent 561e6e3 commit 52ef272

22 files changed

Lines changed: 1540 additions & 84 deletions

BEHAVIOR.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Behavior Specification
2+
3+
> How CodeWalk behaves from the user's perspective.
4+
> Only documents **current, implemented** behavior. Planned features live in ROADMAP.md.
5+
6+
---
7+
8+
## Onboarding
9+
10+
### First launch shows setup wizard
11+
12+
- **Given** the app is opened for the first time (no servers configured)
13+
- **When** the app starts
14+
- **Then** a setup wizard is displayed requiring the user to configure at least one OpenCode server
15+
16+
### No server = no functionality
17+
18+
- **Given** no server is configured
19+
- **When** the user tries to access any feature
20+
- **Then** the app blocks access — configuring a server is a prerequisite for all functionality
21+
22+
---
23+
24+
## Servers
25+
26+
### Multiple server profiles
27+
28+
- **Given** the user is in server settings
29+
- **When** the user adds a new server profile (local, remote, work, etc.)
30+
- **Then** the profile is saved and the user can switch between profiles at any time
31+
32+
### Automatic health checks
33+
34+
- **Given** server profiles are configured
35+
- **When** the app is active
36+
- **Then** the app periodically checks each server's health and shows a visual online/offline indicator
37+
38+
### Server goes offline during use
39+
40+
- **Given** the active server goes offline while the user is chatting
41+
- **When** the connection is lost
42+
- **Then** the composer input is blocked and the reason is displayed to the user
43+
- **Then** the user cannot send messages until the connection is restored
44+
45+
> **Current state**: the offline error is too aggressive (full-screen error with "Retry"). The desired behavior is a subtle composer block with a clear reason message.
46+
47+
---
48+
49+
## Sessions
50+
51+
### Session lifecycle
52+
53+
- **Given** a connected server
54+
- **When** the user interacts with sessions
55+
- **Then** the user can **create**, **rename**, **archive**, **fork**, and **delete** sessions
56+
57+
### Fork creates an independent copy
58+
59+
- **Given** an existing session with conversation history
60+
- **When** the user forks the session
61+
- **Then** a new independent session is created as a copy from that point — changes to either session do not affect the other
62+
63+
### Sessions are scoped to a project
64+
65+
- **Given** the user has multiple projects/workspaces
66+
- **When** the user switches to a different project
67+
- **Then** the visible session list changes to show only sessions belonging to that project
68+
69+
### Auto-generated session titles
70+
71+
- **Given** a new session with no custom title
72+
- **When** the conversation progresses
73+
- **Then** the app automatically generates a title based on the conversation content
74+
75+
---
76+
77+
## Chat
78+
79+
### Messages are streamed in real time
80+
81+
- **Given** a connected server and an active session
82+
- **When** the user sends a message
83+
- **Then** the message is sent to the OpenCode server and the assistant's response streams back via SSE, rendering in real time as text arrives
84+
85+
### User can cancel a response
86+
87+
- **Given** the assistant is actively streaming a response
88+
- **When** the user taps the cancel/stop button
89+
- **Then** the response generation stops and the partial response remains visible
90+
91+
### Sending while processing enqueues messages
92+
93+
- **Given** the assistant is actively streaming a response
94+
- **When** the user sends one or more new messages
95+
- **Then** each new message is shown in the timeline immediately as queued (with `Queued` state)
96+
- **Then** the app does not interrupt the running response automatically
97+
98+
### Queued messages are batched into one send
99+
100+
- **Given** there are multiple queued messages for the same session
101+
- **When** the queue is dispatched (either on first idle opportunity or via `Send now`)
102+
- **Then** all queued message texts are merged and sent as a single payload
103+
- **Then** each original queued message becomes one line in that payload (simple `\n` line breaks between messages)
104+
105+
### Send now forces immediate queued dispatch
106+
107+
- **Given** there are queued messages while the assistant is still processing
108+
- **When** the user taps `Send now`
109+
- **Then** the app performs the same stop behavior as `Stop` for the active response
110+
- **Then** as soon as the session becomes ready, the app sends the full queued batch as one payload
111+
112+
### Failed send returns message to composer
113+
114+
- **Given** the user sends a message
115+
- **When** the send fails (network error, server error, etc.)
116+
- **Then** the message text is returned to the composer input — the user's text is never lost
117+
118+
### Tool call work groups collapse after completion
119+
120+
- **Given** the assistant executes tool calls during a response (file reads, commands, etc.)
121+
- **When** the assistant finishes the complete response
122+
- **Then** the intermediate tool call groups collapse to keep the chat clean
123+
- **Then** collapse never happens while the assistant is still streaming
124+
125+
### UI remains fluid during streaming
126+
127+
- **Given** the assistant is streaming a long response
128+
- **When** text, code blocks, or tool calls render incrementally
129+
- **Then** the UI remains smooth without stuttering, freezing, or perceptible lag
130+
131+
---
132+
133+
## Composer
134+
135+
### Message history navigation
136+
137+
- **Given** the user has sent previous messages in the session
138+
- **When** the user presses the up/down arrow keys in the composer
139+
- **Then** the composer cycles through previously sent messages
140+
141+
### File mentions with @
142+
143+
- **Given** the user is typing in the composer
144+
- **When** the user types `@`
145+
- **Then** a file/context mention picker appears, allowing the user to reference project files in the message
146+
147+
### Slash commands with /
148+
149+
- **Given** the user is typing in the composer
150+
- **When** the user types `/`
151+
- **Then** a command picker appears with available slash commands
152+
153+
---
154+
155+
## Attachments
156+
157+
### Image and PDF attachments
158+
159+
- **Given** the user is composing a message
160+
- **When** the user attaches an image or PDF
161+
- **Then** the file is attached to the message and sent along with the text
162+
163+
### Model capability gating
164+
165+
- **Given** the selected model does not support vision
166+
- **When** the user tries to attach an image
167+
- **Then** the attachment option is disabled or shows clear feedback that the model cannot process images
168+
169+
---
170+
171+
## Voice Input
172+
173+
### Speech-to-text in the composer
174+
175+
- **Given** the user activates voice input
176+
- **When** the user speaks
177+
- **Then** the speech is converted to text and inserted into the composer input
178+
179+
### Cross-platform support
180+
181+
- **Given** any supported platform (Android, Linux, macOS, Windows, Web)
182+
- **When** the user activates voice input
183+
- **Then** the STT feature works on all platforms where the device has a microphone
184+
185+
---
186+
187+
## Interactive Prompts
188+
189+
### Permission requests
190+
191+
- **Given** the server needs user approval to perform an action (e.g., execute a command, write a file)
192+
- **When** the server sends a permission request
193+
- **Then** an interactive card appears in the chat with approve/deny options
194+
- **Then** the server waits for the user's response before proceeding
195+
196+
### Question prompts
197+
198+
- **Given** the server needs the user to choose between options
199+
- **When** the server sends a question prompt
200+
- **Then** an interactive card appears with the question and selectable options
201+
- **Then** the server waits for the user's response before proceeding
202+
203+
---
204+
205+
## File Explorer
206+
207+
### Read-only project tree
208+
209+
- **Given** the user opens the file explorer panel
210+
- **When** the project tree loads
211+
- **Then** the user sees the file/folder structure of the current project in read-only mode (no create, edit, or delete)
212+
213+
### File preview
214+
215+
- **Given** the file explorer is open
216+
- **When** the user taps a file
217+
- **Then** a preview/visualization of the file content is shown
218+
219+
---
220+
221+
## Task List
222+
223+
### Agent-controlled task list
224+
225+
- **Given** the AI agent is executing a multi-step task
226+
- **When** the agent reports its task progress
227+
- **Then** a task list is displayed in the session showing the agent's current and completed steps
228+
- **Then** the task list is read-only for the user — it is controlled entirely by the server/agent
229+
230+
---
231+
232+
## Layout
233+
234+
### Mobile: chat-first with drawer
235+
236+
- **Given** the app is running on a mobile device (compact screen)
237+
- **When** the user navigates the app
238+
- **Then** the chat occupies the full screen, with the session list accessible via a lateral drawer
239+
240+
### Desktop: split view
241+
242+
- **Given** the app is running on a desktop (expanded screen)
243+
- **When** the user navigates the app
244+
- **Then** the session list is always visible alongside the chat in a split-view layout
245+
246+
### Keyboard shortcuts
247+
248+
- **Given** a physical keyboard is connected (desktop or mobile with external keyboard)
249+
- **When** the user presses a keyboard shortcut
250+
- **Then** the corresponding action is executed (shortcuts work on desktop and on mobile with an external keyboard)
251+
252+
### Mobile keyboard collapses auxiliary panels
253+
254+
- **Given** an auxiliary panel is open (task list, drawer, etc.) on mobile
255+
- **When** the on-screen keyboard appears
256+
- **Then** auxiliary panels auto-collapse to maximize available space for the chat and composer
257+
258+
---
259+
260+
## Settings
261+
262+
### Theme selection
263+
264+
- **Given** the user is in settings
265+
- **When** the user selects a theme
266+
- **Then** the app supports light, dark, and AMOLED themes, plus Material You dynamic color from the system wallpaper
267+
268+
### Local persistence
269+
270+
- **Given** the user changes any setting
271+
- **When** the setting is saved
272+
- **Then** it persists locally (survives app restart) via SharedPreferences / SecureStorage
273+
274+
---
275+
276+
## Notifications
277+
278+
### Background notifications
279+
280+
- **Given** the app is in the background
281+
- **When** the assistant finishes a response → push notification
282+
- **When** a permission request arrives → push notification
283+
- **When** a question prompt arrives → push notification
284+
285+
### Server offline does NOT notify
286+
287+
- **Given** the active server goes offline
288+
- **When** the app detects the disconnection
289+
- **Then** no push notification is sent — server availability is not the app's responsibility. The user sees the status when they open the app.
290+
291+
### Android persistent notification
292+
293+
- **Given** the app is running on Android
294+
- **When** the app is active or in background
295+
- **Then** a persistent notification acts as a tray icon, enabling reliable notification delivery
296+
297+
---
298+
299+
## Background and Lifecycle
300+
301+
### Android foreground service
302+
303+
- **Given** the app is running on Android during a long operation
304+
- **When** the app goes to background
305+
- **Then** a foreground service keeps the app alive so the operation is not killed by the system
306+
307+
### Battery optimization prompt
308+
309+
- **Given** the app is running on Android
310+
- **When** battery optimization may interfere with background operation
311+
- **Then** the app prompts the user to disable battery optimization
312+
313+
### Automatic reconnection on resume
314+
315+
- **Given** the app was in background
316+
- **When** the user returns to the app
317+
- **Then** the app automatically reconnects to the server and resynchronizes state (missed messages, updated sessions, etc.)
318+
319+
### No duplicate refresh on resume
320+
321+
- **Given** the app resumes from background
322+
- **When** both lifecycle and reconnect triggers fire
323+
- **Then** only one refresh cycle executes — no duplicate network calls
324+
325+
---
326+
327+
## Anti-behaviors
328+
329+
> Things that must **never** happen, regardless of circumstances.
330+
331+
### Never lose user messages
332+
333+
The app must never silently discard a user's message. If sending fails, the message text returns to the composer input.
334+
335+
### Never freeze the UI
336+
337+
All operations (streaming, sync, network) are asynchronous. The UI must never become unresponsive, even during heavy operations.
338+
339+
### Never expose tokens or credentials
340+
341+
Server tokens, API keys, and credentials must never appear in logs, error screens, exports, or any user-visible surface.
342+
343+
### Never auto-approve permissions
344+
345+
Permission requests from the server always require explicit user action (approve or deny). The app must never approve automatically.
346+
347+
### Never show false aborts
348+
349+
When a connection drops and reconnects (especially on mobile background/resume), the app must not display false "message aborted" errors from stale SSE events.
350+
351+
### Never corrupt state on rapid actions
352+
353+
If the user taps rapidly (double-tap on sessions, fast project switching), the app processes one transition at a time. Concurrent transitions must never corrupt state or cause navigation errors.
354+
355+
### Never cancel responses on session switch
356+
357+
If the assistant is streaming a response and the user switches to a different session, the in-flight response must be preserved — not cancelled. The user can return to the original session and see the completed response.
358+
359+
### Never collapse work groups during streaming
360+
361+
Tool call work groups must only collapse after the assistant has fully completed its response. Premature collapse causes visual flicker and hides active work.
362+
363+
### Never show stale data after resume
364+
365+
When the app returns from background, it must refresh the current session to show the latest state. However, refresh must not re-inject stale abort data that was already handled.
366+
367+
### Never break layout with keyboard
368+
369+
On mobile, the on-screen keyboard must never cause overflow, clipping, or layout breakage. Fixed minimum heights must account for the keyboard-reduced viewport.
370+
371+
### Errors: only show blocking ones
372+
373+
The user should see error feedback only when the error prevents them from continuing (send failed, server unreachable). Non-blocking warnings from the server (partial timeouts, transient issues) should be silent.

CODEBASE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ tool/ci/check_coverage.sh # Coverage threshold gate (default: 35%)
245245
- Android build targets Java 17 (`sourceCompatibility`, `targetCompatibility`, `jvmTarget`).
246246
- featM icon migration completed in `lib/presentation/**` and `test/widget/**`: Material icons moved from `Icons.*` to `Symbols.*` (`material_symbols_icons`).
247247

248+
### Queued Send Flow
249+
250+
- **Provider queue state + drain/send-now**: `chat_provider.dart` tracks per-session queued envelopes/local queued IDs and drains them as a merged batch; `sendQueuedNow()` can stop active response and force immediate drain.
251+
- **Composer queued status + action**: `chat_page_composer_status.dart` prioritizes queued-count status, and `chat_page_composer_widgets.dart` renders queued UI with a `Send now` action.
252+
- **Queued message badge**: `chat_message_widget.dart` + `chat_message_content.dart` expose/render `isQueuedUserMessage` with a `Queued` badge in user message bubbles.
253+
248254
### Android Background Monitoring
249255

250256
- **Native foreground service** (`android/app/src/main/kotlin/com/verseles/codewalk/CodeWalkForegroundService.kt`):

ROADMAP.featQ.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
- [ ] Plan the merge between the project selector and the conversations sidebar, grouping conversations by open projects to speed up navigation.
6060
- [ ] Tasks widget gets a footer progress bar based on total completed items.
6161
- [x] Plan support for sending a new message while the assistant is still working (OpenCode CLI/Web parity), following existing OpenCode interaction patterns. - Planning doc: `ROADMAP.featQ.concurrent-send-plan.md`
62+
- [x] Deliver queued-send behavior while processing: queue new messages during active run; `Send now` aborts current run and sends queued content; multiple queued drafts merge into one payload with single `\n` separators; show queued status/indicators in composer/chat UI. - Related commits: dfd64eb f8df06a c8884b2
6263
- [ ] Allow pinning sessions in the Conversations sidebar.
6364

6465
## Goal

0 commit comments

Comments
 (0)