Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcac327f0e | ||
|
|
d64a33f7ef | ||
|
|
9eea81632a | ||
|
|
1e4911d323 | ||
|
|
5ae555b51e |
@@ -13,27 +13,23 @@ Automation is handled by [agentloop](https://github.com/guettli/agentloop) runni
|
||||
| Label | Trigger | Outcome |
|
||||
|---|---|---|
|
||||
| `loop/plan` | Planning agent reads the issue and writes an implementation plan as a comment | Issue moves to `loop/plan-done` |
|
||||
| `loop/code` | Coding agent implements the change, creates a branch + PR | Issue routes to `loop/merge` |
|
||||
| `loop/merge` | Merge agent rebases, waits for CI, and merges the PR | Issue moves to `loop/merge-done` |
|
||||
| `loop/code` | Coding agent implements the change, creates a branch + PR | Issue moves to `loop/code-done` |
|
||||
|
||||
**State machine:**
|
||||
|
||||
```
|
||||
loop/plan → loop/plan-in-process → loop/plan-done
|
||||
↘ NeedSupervisor (on failure)
|
||||
loop/plan → loop/plan-in-progress → loop/plan-done
|
||||
↘ NeedSupervisor (on failure)
|
||||
|
||||
loop/code → loop/code-in-process → loop/merge (via route)
|
||||
↘ NeedSupervisor (on failure)
|
||||
|
||||
loop/merge → loop/merge-in-process → loop/merge-done
|
||||
↘ NeedSupervisor (on failure)
|
||||
loop/code → loop/code-in-progress → loop/code-done
|
||||
↘ NeedSupervisor (on failure)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Only issues authored by allowed users are picked up (guettli, guettlibot, guettlibot2, forgejo-actions).
|
||||
- An issue with `NeedSupervisor` needs human attention — investigate, fix, then re-label.
|
||||
- The merge agent merges the PR automatically once CI is green. A human still reviews the PR before it merges if branch protection requires a review.
|
||||
- The coding agent opens a PR but does NOT close the issue. A human reviews the PR and closes the issue after merging.
|
||||
- Planning agents only post a comment — they do NOT write code or open PRs.
|
||||
- `loop/*` labels are managed by agentloop — do not set them manually while an agent is active.
|
||||
|
||||
@@ -43,9 +39,9 @@ loop/merge → loop/merge-in-process → loop/merge-done
|
||||
1. Create issue
|
||||
2. Add label loop/plan → agent writes plan as comment
|
||||
3. Review plan, request changes or approve
|
||||
4. Add label loop/code → agent implements + opens PR + hands off to merge
|
||||
5. (Optional) Review PR before it merges
|
||||
6. Merge agent waits for CI and merges the PR automatically
|
||||
4. Add label loop/code → agent implements + opens PR
|
||||
5. Review PR, merge
|
||||
6. Close issue
|
||||
```
|
||||
|
||||
## Code conventions
|
||||
|
||||
@@ -278,7 +278,14 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
|
||||
),
|
||||
],
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: _runSearch,
|
||||
onSubmitted: (value) {
|
||||
// Only run the search if results haven't settled yet via
|
||||
// onChanged — prevents a second IMAP round-trip from reordering
|
||||
// the already-visible results when the user presses Enter.
|
||||
if (_searchResults == null && !_searchLoading) {
|
||||
unawaited(_runSearch(value));
|
||||
}
|
||||
},
|
||||
textInputAction: TextInputAction.search,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -798,6 +798,67 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'pressing Enter after search settles does not reorder results',
|
||||
(tester) async {
|
||||
// Reproduces: user types a query → onChanged fires → results settle.
|
||||
// Then user presses Enter → onSubmitted fires a second search → the
|
||||
// second IMAP response may return results in a different order, so the
|
||||
// tile the user is about to tap is no longer the email they expect.
|
||||
final email1 = testEmail(id: 'acc-1:1', subject: 'Alpha Foo');
|
||||
final email2 = testEmail(id: 'acc-1:2', subject: 'Beta Foo');
|
||||
var callCount = 0;
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||
overrides: [
|
||||
accountRepositoryProvider.overrideWithValue(
|
||||
FakeAccountRepository([kTestAccount]),
|
||||
),
|
||||
mailboxRepositoryProvider.overrideWithValue(
|
||||
FakeMailboxRepository(),
|
||||
),
|
||||
emailRepositoryProvider.overrideWithValue(
|
||||
FakeEmailRepository(
|
||||
onSearch: (_) async {
|
||||
callCount++;
|
||||
// First call: [Alpha, Beta]. Second call: reversed.
|
||||
return callCount == 1 ? [email1, email2] : [email2, email1];
|
||||
},
|
||||
emailBody: const EmailBody(emailId: '', attachments: []),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Typing triggers onChanged → first search → results settle.
|
||||
await tester.enterText(find.byType(TextField), 'foo');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Alpha Foo'), findsOneWidget);
|
||||
expect(find.text('Beta Foo'), findsOneWidget);
|
||||
// Alpha must appear above Beta (it is first in the list).
|
||||
expect(
|
||||
tester.getTopLeft(find.text('Alpha Foo')).dy,
|
||||
lessThan(tester.getTopLeft(find.text('Beta Foo')).dy),
|
||||
);
|
||||
|
||||
// Pressing Enter triggers onSubmitted — must NOT re-run the search.
|
||||
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Order must be unchanged: pressing Enter must not reorder results.
|
||||
expect(find.text('Alpha Foo'), findsOneWidget);
|
||||
expect(find.text('Beta Foo'), findsOneWidget);
|
||||
expect(
|
||||
tester.getTopLeft(find.text('Alpha Foo')).dy,
|
||||
lessThan(tester.getTopLeft(find.text('Beta Foo')).dy),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('shows preview snippet when email has preview', (tester) async {
|
||||
final email = Email(
|
||||
id: 'acc-1:99',
|
||||
|
||||
Reference in New Issue
Block a user