Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ class BackgroundSetupJobs extends _$BackgroundSetupJobs
issueUrl: issueUrl,
),
launchAgent: runtime.launchAgent,
assignSection: (workspaceId, sectionId) =>
controller.saveWorkspaceSection(workspaceId, sectionId: sectionId),
onPhase: (phase) => _setPhase(id, phase),
);
try {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class const PromptWorkspacePipeline({
required String operationId,
required String projectId,
required String prompt,
required bool autoAssignSection,
})
generateIdentity,
required final Future<bool> Function(Project project, String branchName)
Expand All @@ -52,6 +53,8 @@ class const PromptWorkspacePipeline({
required bool requireIdempotency,
})
launchAgent,
final Future<void> Function(String workspaceId, String sectionId)?
assignSection,
final void Function(String phase)? onPhase,
final String Function()? createOperationId,
}) {
Expand All @@ -70,6 +73,7 @@ class const PromptWorkspacePipeline({
operationId: createOperationId?.call() ?? const Uuid().v4(),
projectId: request.project.id,
prompt: identityPrompt,
autoAssignSection: request.autoAssignSection,
);
onPhase?.call('Checking generated branch');
final collision =
Expand All @@ -93,6 +97,16 @@ class const PromptWorkspacePipeline({
hostId: request.hostId,
issueUrl: request.issueUrl,
);
final sectionId = request.autoAssignSection ? identity.sectionId : null;
final sectionAssignment = assignSection;
if (sectionId != null && sectionAssignment != null) {
try {
await sectionAssignment(creation.workspace.id, sectionId);
} catch (_) {
// Section assignment is best-effort: the workspace itself was
// already created, so a failure must not fail the flow.
}
}
break;
} catch (error) {
if (attempt == 0 && _looksLikeCollision(error)) {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/features/workbench/domain/background_setup_job.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class const PromptWorkspaceCreateRequest({
final String? parentWorkspaceId,
final String? hostId,
final String? issueUrl,
final bool autoAssignSection = false,
final WorkspaceCreationResult? created,
final String? clientMutationId,
final bool? originalLaunchWasIdempotent,
Expand All @@ -56,6 +57,7 @@ class const PromptWorkspaceCreateRequest({
parentWorkspaceId: parentWorkspaceId,
hostId: hostId,
issueUrl: issueUrl,
autoAssignSection: autoAssignSection,
created: created,
clientMutationId: clientMutationId ?? this.clientMutationId,
originalLaunchWasIdempotent:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:alera/src/features/workbench/infra/terminal_host/terminal_host_p
class const GeneratedWorkspaceIdentity({
required final String workspaceName,
required final String branchName,
final String? sectionId,
});

class const AgentProfileLaunchResult({
Expand All @@ -27,6 +28,7 @@ class PromptWorkspaceRuntimeClient(
required String projectId,
required String prompt,
String? tabId,
bool autoAssignSection = false,
}) async {
await beforeAccess?.call();
final payload = _asMap(
Expand All @@ -37,13 +39,17 @@ class PromptWorkspaceRuntimeClient(
'projectId': projectId,
'prompt': prompt,
'tabId': ?tabId,
// Additive: an older host ignores the flag and simply omits
// sectionId from the response.
'autoAssignSection': autoAssignSection,
},
const Duration(minutes: 11),
),
);
return GeneratedWorkspaceIdentity(
workspaceName: _requiredString(payload, 'workspaceName'),
branchName: _requiredString(payload, 'branchName'),
sectionId: _optionalString(payload, 'sectionId'),
);
}

Expand Down Expand Up @@ -129,3 +135,11 @@ String _requiredString(Map<String, Object?> value, String key) {
}
throw FormatException('Runtime response is missing "$key".');
}

String? _optionalString(Map<String, Object?> value, String key) {
final field = value[key];
if (field is String && field.trim().isNotEmpty) {
return field;
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class const PromptWorkspaceDialog({
required String operationId,
required String projectId,
required String prompt,
required bool autoAssignSection,
Comment thread
leynier marked this conversation as resolved.
})
generateIdentity,
required final Future<void> Function(String operationId) cancelGeneration,
Expand Down Expand Up @@ -102,6 +103,10 @@ class const PromptWorkspaceDialog({
final String? initialError,
final NewWorkspaceMode initialMode = .fromPrompt,
final Widget? manualForm,
final bool hasWorkspaceSections = false,
final bool initialAutoAssignSection = true,
final Future<void> Function(String workspaceId, String sectionId)?
assignSection,
}) extends StatefulWidget {
@override
State<PromptWorkspaceDialog> createState() => _PromptWorkspaceDialogState();
Expand Down Expand Up @@ -129,11 +134,16 @@ class _PromptWorkspaceDialogState extends State<PromptWorkspaceDialog> {
String? _agentLaunchMutationId;
bool? _originalAgentLaunchWasIdempotent;
bool _createAnother = false;
bool _autoAssignSection = true;
bool _useProjectCheckout = false;

bool get _autoAssignSectionEffective =>
widget.hasWorkspaceSections && _autoAssignSection;
Comment thread
leynier marked this conversation as resolved.

@override
void initState() {
super.initState();
_autoAssignSection = widget.initialAutoAssignSection;
_useProjectCheckout =
widget.enqueuePrompt != null && widget.initialUseProjectCheckout;
_mode = widget.initialMode;
Expand Down Expand Up @@ -295,6 +305,7 @@ class _PromptWorkspaceDialogState extends State<PromptWorkspaceDialog> {
parentWorkspaceId: _selectedParentWorkspaceId,
hostId: _selectedHostId,
issueUrl: _linkedIssueUrl(),
autoAssignSection: _autoAssignSectionEffective,
),
);
if (done == null) {
Expand Down Expand Up @@ -353,6 +364,7 @@ class _PromptWorkspaceDialogState extends State<PromptWorkspaceDialog> {
operationId: operationId,
projectId: project.id,
prompt: identityPrompt,
autoAssignSection: _autoAssignSectionEffective,
);
} finally {
if (_activeOperationId == operationId) {
Expand Down Expand Up @@ -388,6 +400,18 @@ class _PromptWorkspaceDialogState extends State<PromptWorkspaceDialog> {
hostId: _selectedHostId,
issueUrl: _linkedIssueUrl(),
);
final sectionId = _autoAssignSectionEffective
? identity.sectionId
: null;
final assignSection = widget.assignSection;
if (sectionId != null && assignSection != null) {
try {
await assignSection(creation.workspace.id, sectionId);
} catch (_) {
// Section assignment is best-effort: the workspace itself was
// already created, so a failure must not fail the flow.
}
}
break;
} catch (error) {
if (attempt == 0 && _looksLikeCollision(error)) {
Expand Down Expand Up @@ -444,42 +468,6 @@ class _PromptWorkspaceDialogState extends State<PromptWorkspaceDialog> {
message.contains('workspace for branch');
}

Future<void> _cancelGeneration() async {
final operationId = _activeOperationId;
if (operationId != null) {
await widget.cancelGeneration(operationId);
}
}

Future<void> _finishCreation(
WorkspaceCreationResult creation,
String agentTabId,
) async {
if (!_createAnother) {
Navigator.of(context).pop(
PromptWorkspaceDialogResult(creation: creation, agentTabId: agentTabId),
);
return;
}
await widget.onCreateAnother?.call(
creation: creation,
agentTabId: agentTabId,
);
if (!mounted) {
return;
}
_promptController.clear();
_issueUrlController.clear();
_agentLaunchMutationId = null;
_originalAgentLaunchWasIdempotent = null;
setState(() {
_working = false;
_phase = null;
_error = null;
_created = null;
});
}

void _selectMode(NewWorkspaceMode mode) {
setState(() => _mode = mode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,40 @@ extension _PromptWorkspaceDialogAgentLaunch on _PromptWorkspaceDialogState {
}
}
}

Future<void> _cancelGeneration() async {
final operationId = _activeOperationId;
if (operationId != null) {
await widget.cancelGeneration(operationId);
}
}

Future<void> _finishCreation(
WorkspaceCreationResult creation,
String agentTabId,
) async {
if (!_createAnother) {
Navigator.of(context).pop(
PromptWorkspaceDialogResult(creation: creation, agentTabId: agentTabId),
);
return;
}
await widget.onCreateAnother?.call(
creation: creation,
agentTabId: agentTabId,
);
if (!mounted) {
return;
}
_promptController.clear();
_issueUrlController.clear();
_agentLaunchMutationId = null;
_originalAgentLaunchWasIdempotent = null;
_update(() {
_working = false;
_phase = null;
_error = null;
_created = null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,28 @@ extension _PromptWorkspaceDialogForm on _PromptWorkspaceDialogState {
),
],
const SizedBox(height: AleraTokens.space12),
AleraCheckbox(
value: _createAnother,
enabled: !_working && created == null,
onChanged: (value) {
_update(() => _createAnother = value);
},
label: 'Create Another',
Row(
children: <Widget>[
if (widget.hasWorkspaceSections) ...<Widget>[
AleraCheckbox(
value: _autoAssignSection,
enabled: !_working && created == null,
onChanged: (value) {
_update(() => _autoAssignSection = value);
},
label: 'Auto Assign Section',
),
const SizedBox(width: AleraTokens.space12),
],
AleraCheckbox(
value: _createAnother,
enabled: !_working && created == null,
onChanged: (value) {
_update(() => _createAnother = value);
},
label: 'Create Another',
),
],
),
const SizedBox(height: AleraTokens.space20),
Row(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ Future<void> _showCreateWorkspaceDialogs(
String? remainingRetryJobId,
}) async {
var boundJobId = remainingRetryJobId;
final workbenchState = ref.read(workbenchControllerProvider);
final hasWorkspaceSections =
workbenchState.supportsSections && workbenchState.sections.isNotEmpty;
final promptResult = await showDialog<PromptWorkspaceDialogResult>(
context: context,
builder: (_) => PromptWorkspaceDialog(
Expand Down Expand Up @@ -183,6 +186,10 @@ Future<void> _showCreateWorkspaceDialogs(
for (final candidate in parentCandidates) candidate.workspace,
],
initialUseProjectCheckout: retryPrompt?.useProjectCheckout ?? true,
hasWorkspaceSections: hasWorkspaceSections,
initialAutoAssignSection: retryPrompt?.autoAssignSection ?? true,
assignSection: (workspaceId, sectionId) =>
controller.saveWorkspaceSection(workspaceId, sectionId: sectionId),
generateIdentity: runtime.generateIdentity,
cancelGeneration: runtime.cancel,
createWorkspace:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class const AgentProfileSummary({
class const GeneratedWorkspaceIdentity({
required final String workspaceName,
required final String branchName,
final String? sectionId,
});

class const AgentProfileLaunchResult({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ abstract interface class MobileWorkspaceClient {
required String operationId,
required String projectId,
required String prompt,
bool autoAssignSection = false,
});
Future<void> cancelWorkspaceIdentity(String operationId);
Future<PromptImageUploadResult> uploadPromptImage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,19 +299,24 @@ mixin MobileRuntimeWorkspaceClient {
required String operationId,
required String projectId,
required String prompt,
bool autoAssignSection = false,
}) async {
final payload = await requestMap(
'aiText.workspaceIdentity.generate',
<String, Object?>{
'operationId': operationId,
'projectId': projectId,
'prompt': prompt,
// Additive: an older host ignores the flag and simply omits
// sectionId from the response.
'autoAssignSection': autoAssignSection,
},
const Duration(minutes: 11),
);
return GeneratedWorkspaceIdentity(
workspaceName: payload.requiredString('workspaceName'),
branchName: payload.requiredString('branchName'),
sectionId: payload.optionalString('sectionId'),
);
}

Expand Down
Loading
Loading