pick-up sets the ticket to in_progress on accept (T-339)

When a ticket is handed to a live Claude pane (T-327), advance it to
in_progress on the receiving side of the bus — gated on acceptance, so a
pick-up with no live session stays a quiet no-op and never mutates state.
Only a not-yet-started ticket (backlog/ready) transitions, so re-picking
up a review/done ticket doesn't drag it backwards. On success it publishes
(builtin.tickets, changed) so the sidebar refreshes.

The handler logic moves into a testable applyTicketPickUp() seam; the
sidebar button now carries the current status in the pick-up payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 16:18:16 +02:00
co-authored by Claude Opus 4.8
parent dea3e70e49
commit a948bc8a1f
6 changed files with 173 additions and 8 deletions
+44 -7
View File
@@ -354,14 +354,13 @@ class ClaudeExtension extends ClideExtension {
_subs.add(ctx.messages.subscribe(publisher: 'builtin.tickets', channel: 'pick-up').listen(_onTicketPickUp));
}
/// Hand a picked-up ticket to the active Claude session (T-327): the primary
/// lead, else the first visible session. A quiet no-op when none is live.
/// Hand a picked-up ticket to the active Claude session (T-327/T-339). The
/// decision + transition live in [applyTicketPickUp] so they're testable
/// without the activation machinery.
void _onTicketPickUp(Message m) {
final prompt = m.data['prompt'] as String?;
if (prompt == null || prompt.isEmpty) return;
final target = _orchestrator?.byId('primary') ?? _orchestrator?.visibleSessions.firstOrNull;
if (target == null) return;
_orchestrator?.injectMessage(target.id, prompt);
final ctx = _ctx;
if (ctx == null) return;
unawaited(applyTicketPickUp(m.data, orchestrator: _orchestrator, ipc: ctx.ipc, messages: ctx.messages));
}
/// Close every session that doesn't belong to the newly-active workspace
@@ -472,3 +471,41 @@ class ClaudeExtension extends ClideExtension {
return IpcResponse.ok(id: '', data: const {'status': 'shown'});
}
}
/// Statuses a pick-up may advance from: a not-yet-started ticket. Picking up a
/// ticket that's already `in_progress`/`review`/`done`/`cancelled` injects the
/// prompt but leaves the status alone, so a re-pick-up never drags it backwards
/// or reopens it (T-339).
const _kPickUpStartableStatuses = {'backlog', 'ready'};
/// Inject a picked-up ticket's prompt into the active session (the `primary`
/// lead, else the first visible one) and, on acceptance from a not-yet-started
/// ticket, advance it to `in_progress` and publish a `changed` so the sidebar
/// refreshes (T-327/T-339). Returns whether a live session accepted the prompt.
///
/// With no live session there's no injection and no state change — a quiet
/// no-op. Kept free of the extension lifecycle so it's directly testable.
@visibleForTesting
Future<bool> applyTicketPickUp(
Map<String, Object?> data, {
required ClaudeSessionOrchestrator? orchestrator,
required DaemonClient ipc,
required MessageBus messages,
}) async {
final prompt = data['prompt'] as String?;
if (prompt == null || prompt.isEmpty) return false;
final target = orchestrator?.byId('primary') ?? orchestrator?.visibleSessions.firstOrNull;
if (target == null) return false; // no live session → quiet no-op, no state change
orchestrator!.injectMessage(target.id, prompt);
final id = data['id'] as String?;
final status = data['status'] as String?;
if (id != null && id.isNotEmpty && _kPickUpStartableStatuses.contains(status)) {
final resp = await ipc.request('pql.tickets.status', args: {
'ids': [id],
'status': 'in_progress',
});
if (resp.ok) messages.publish('builtin.tickets', 'changed', {'id': id});
}
return true;
}
+3 -1
View File
@@ -328,7 +328,9 @@ class _PickUpAction extends StatelessWidget {
unawaited(() async {
final resp = await kernel.ipc.request('pql.tickets.show', args: {'id': id, 'withContext': true});
if (!resp.ok) return; // a missing ticket / failed fetch is a quiet no-op
kernel.messages.publish('builtin.tickets', 'pick-up', {'id': id, 'prompt': pickUpPrompt(resp.data)});
// Carry the current status so the receiver can gate the in_progress
// transition without a second fetch (T-339).
kernel.messages.publish('builtin.tickets', 'pick-up', {'id': id, 'prompt': pickUpPrompt(resp.data), 'status': resp.data['status']});
}());
},
builder: (ctx, hovered, _) => Padding(