// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #include "codex/NewThreadDialog.h" #include "codex/FileSelectionDialog.h" #include "codex/ui/UiStyle.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace codexui::codex { namespace { QLabel *label(QString text, const char *kind = "body") { auto *value = new QLabel(std::move(text)); value->setProperty("kind", kind); value->setWordWrap(true); return value; } QWidget *field(QString caption, QWidget *control, QWidget *buddy = nullptr) { auto *widget = new QWidget; auto *layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(6); auto *captionLabel = label(std::move(caption), "title"); captionLabel->setBuddy(buddy ? buddy : control); captionLabel->buddy()->setAccessibleName(captionLabel->text()); layout->addWidget(captionLabel); layout->addWidget(control); return widget; } } // namespace NewThreadDialog::NewThreadDialog(QString initialWorkspace, QWidget *parent) : NewThreadDialog( NewThreadDraft{std::move(initialWorkspace), {}, {}, {}, false}, Purpose::Create, parent) {} NewThreadDialog::NewThreadDialog(NewThreadDraft initialDraft, Purpose purpose, QWidget *parent) : UiStyle::ScrollFormDialog(parent) { setModal(true); const bool forFork = purpose == Purpose::Fork; const QString heading = forFork ? QStringLiteral("Fork with options") : QStringLiteral("New thread"); setWindowTitle(heading); auto *root = new QVBoxLayout(this); root->setContentsMargins(24, 22, 24, 20); root->setSpacing(14); root->addWidget(label(heading, "heading")); root->addWidget(label( forFork ? QStringLiteral("Adjust the copied thread context. Model, access, " "reasoning, and style remain in the upcoming-turn " "controls.") : QStringLiteral( "Set the thread context. Model, access, reasoning, and " "style remain in the upcoming-turn controls."), "muted")); auto *scroll = formScrollArea(); scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); auto *content = new QWidget; auto *form = new QVBoxLayout(content); form->setContentsMargins(0, 2, 8, 2); form->setSpacing(16); workspace = new QLineEdit(std::move(initialDraft.workspace)); workspace->setPlaceholderText(QDir::homePath()); auto *workspaceRow = new QWidget; auto *workspaceLayout = new QHBoxLayout(workspaceRow); workspaceLayout->setContentsMargins(0, 0, 0, 0); workspaceLayout->setSpacing(8); auto *browse = new QPushButton(QStringLiteral("Browse")); browse->setMinimumHeight(34); workspaceLayout->addWidget(workspace, 1); workspaceLayout->addWidget(browse); form->addWidget(field(QStringLiteral("Workspace"), workspaceRow, workspace)); name = new QLineEdit; name->setPlaceholderText(QStringLiteral("Optional thread name")); if (!initialDraft.ephemeral) name->setText(std::move(initialDraft.name)); name->setEnabled(!initialDraft.ephemeral); form->addWidget(field(QStringLiteral("Name"), name)); baseInstructions = new UiStyle::DialogTextEdit; baseInstructions->setPlaceholderText( QStringLiteral("Optional base instructions")); baseInstructions->setMaximumHeight(110); baseInstructions->setProperty("kind", "dialogEditor"); baseInstructions->setPlainText(std::move(initialDraft.baseInstructions)); form->addWidget(field(QStringLiteral("Base instructions"), baseInstructions)); developerInstructions = new UiStyle::DialogTextEdit; developerInstructions->setPlaceholderText( QStringLiteral("Optional developer instructions")); developerInstructions->setMaximumHeight(110); developerInstructions->setProperty("kind", "dialogEditor"); developerInstructions->setPlainText( std::move(initialDraft.developerInstructions)); form->addWidget( field(QStringLiteral("Developer instructions"), developerInstructions)); auto *ephemeralSurface = new QFrame; ephemeralSurface->setProperty("kind", "summary"); auto *ephemeralLayout = new QVBoxLayout(ephemeralSurface); ephemeralLayout->setContentsMargins(12, 10, 12, 10); ephemeral = new QCheckBox(QStringLiteral("Temporary thread")); ephemeral->setChecked(initialDraft.ephemeral); ephemeralLayout->addWidget(ephemeral); ephemeralLayout->addWidget( label(QStringLiteral( "Temporary threads are not retained in normal Codex history."), "meta")); form->addWidget(ephemeralSurface); form->addStretch(); scroll->setWidget(content); root->addWidget(scroll, 1); for (auto *editor : {baseInstructions, developerInstructions}) connect(editor, &QPlainTextEdit::cursorPositionChanged, this, [this, editor] { revealFormFocus(editor); }); errorLabel = label({}, "meta"); errorLabel->setProperty("tone", "danger"); errorLabel->hide(); root->addWidget(errorLabel); auto *footer = new QHBoxLayout; footer->addStretch(); auto *cancel = new QPushButton(QStringLiteral("Cancel")); cancel->setProperty("kind", "cancel"); cancel->setMinimumHeight(34); auto *create = new QPushButton(QStringLiteral("Continue")); create->setProperty("kind", "primary"); create->setMinimumHeight(34); footer->addWidget(cancel); footer->addWidget(create); root->addLayout(footer); connect(browse, &QPushButton::clicked, this, [this] { chooseWorkspace(); }); connect(ephemeral, &QCheckBox::toggled, this, [this](bool checked) { name->setEnabled(!checked); }); connect(cancel, &QPushButton::clicked, this, &QDialog::reject); connect(create, &QPushButton::clicked, this, [this] { acceptDraft(); }); QScreen *targetScreen = parent ? parent->screen() : QGuiApplication::primaryScreen(); const QRect available = targetScreen ? targetScreen->availableGeometry() : QRect(0, 0, 1280, 800); constexpr int ScreenMargin = 64; const int maximumWidth = std::max(minimumWidth(), available.width() - ScreenMargin); const int targetWidth = std::clamp(680, minimumWidth(), maximumWidth); resize(targetWidth, minimumHeight()); root->activate(); form->activate(); const int desiredScrollHeight = content->sizeHint().height() + 2 * scroll->frameWidth(); const int desiredHeight = root->sizeHint().height() - scroll->sizeHint().height() + desiredScrollHeight; const int maximumHeight = std::max(minimumHeight(), available.height() - ScreenMargin); resize(targetWidth, std::clamp(desiredHeight, minimumHeight(), maximumHeight)); } NewThreadDraft NewThreadDialog::draft() const { return {QDir::fromNativeSeparators(workspace->text().trimmed()), ephemeral->isChecked() ? QString{} : name->text().trimmed(), baseInstructions->toPlainText().trimmed(), developerInstructions->toPlainText().trimmed(), ephemeral->isChecked()}; } void NewThreadDialog::chooseWorkspace() { FileSelectionDialog dialog(FileSelectionDialog::Mode::Workspace, draft().workspace, {}, this); if (dialog.exec() == QDialog::Accepted) workspace->setText(QDir::toNativeSeparators(dialog.selectedDirectory())); } void NewThreadDialog::acceptDraft() { const QFileInfo selectedWorkspace(draft().workspace); if (!selectedWorkspace.exists() || !selectedWorkspace.isDir()) { errorLabel->setText( QStringLiteral("Select an existing workspace directory.")); errorLabel->show(); return; } accept(); } } // namespace codexui::codex