8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
10#include "absl/strings/str_split.h"
11#include "absl/strings/strip.h"
12#include "absl/time/clock.h"
13#include "absl/time/time.h"
23#include "imgui/imgui.h"
24#include "imgui/misc/cpp/imgui_stdlib.h"
31constexpr const char*
kItemStatuses[] = {
"implemented",
"prototype",
"decided",
34constexpr const char*
kStepKinds[] = {
"story",
"errand",
"dungeon",
35 "shrine",
"boss",
"optional"};
40 int priority, std::function<
void()> draw)
42 name_(std::move(name)),
45 draw_(std::move(draw)) {}
47 std::string
GetId()
const override {
return id_; }
49 std::string
GetIcon()
const override {
return icon_; }
52 return WindowPresentationPolicy::CoreWorkspace();
57 void Draw(
bool* )
override {
73 const char*
const (&options)[N]) {
75 if (ImGui::BeginCombo(label, value->empty() ?
"-" : value->c_str())) {
76 for (
const char* option : options) {
77 const bool selected = *value == option;
78 if (ImGui::Selectable(option, selected)) {
89 if (status ==
"implemented" || status ==
"done" || status ==
"decided") {
92 if (status ==
"prototype" || status ==
"active") {
95 if (status ==
"open") {
102 return absl::FormatTime(
"%Y-%m-%d", absl::Now(), absl::LocalTimeZone());
109 !absl::ParseTime(
"%Y-%m-%d", date, absl::LocalTimeZone(), &due, &error)) {
112 const int64_t days = absl::ToInt64Hours(due - absl::Now()) / 24;
114 return absl::StrFormat(
"%d days past", -days);
116 return absl::StrFormat(
"%d days left", days);
120 return absl::StrJoin(targets,
", ");
124 std::vector<std::string> out;
125 for (absl::string_view part :
126 absl::StrSplit(text,
',', absl::SkipWhitespace())) {
127 out.emplace_back(absl::StripAsciiWhitespace(part));
137 const char* fallback) {
138 auto it = obj.find(key);
139 return it != obj.end() && it->is_string() ? it->get<std::string>()
140 : std::string(fallback);
153 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
164 return absl::OkStatus();
168 return absl::FailedPreconditionError(
169 "Restore the plan's project before saving its edits");
172 std::string disk_source;
178 "The plan file changed outside agent status since it was loaded. "
179 "Your edits are kept; copy them, then Reload to see the file.";
182 expected_source = std::move(disk_source);
192 return absl::OkStatus();
199 return absl::OkStatus();
202 save_error_ =
"Save or discard plan edits before replacing their project";
216 return absl::OkStatus();
220 if (
dirty_ && !discard_changes) {
221 save_error_ =
"Save or explicitly discard plan edits before reloading";
227 load_error_ = std::string(loaded.status().message());
228 return loaded.status();
230 plan_ = std::move(*loaded);
241 return absl::OkStatus();
257 "Open a .yaze project to plan it. The plan is a JSON file named by "
258 "the project's [planning] section.");
270 ?
"This project has no plan file. Create one, or set [planning] "
271 "file= in the .yaze file."
272 :
"The linked plan could not be loaded. Retry after repairing it, or "
273 "create a plan at a new path. Existing files are never replaced.");
275 const std::filesystem::path project_path(
project()->filepath);
277 (project_path.stem().string() +
".plan.json"))
280 ImGui::SetNextItemWidth(-1);
282 if (ImGui::Button(
ICON_MD_ADD " Create plan and link it to the project")) {
283 absl::Status status =
299 ImGui::TextDisabled(
"%s",
plan_path_.c_str());
301 ImGui::BeginDisabled(!
dirty_);
303 absl::Status status =
Save();
306 status.ok() ?
"Plan saved" : std::string(status.message()),
310 ImGui::EndDisabled();
314 ImGui::OpenPopup(
"Discard plan edits?");
319 if (ImGui::BeginPopupModal(
"Discard plan edits?",
nullptr,
320 ImGuiWindowFlags_AlwaysAutoResize)) {
321 ImGui::TextUnformatted(
"Reload discards the unsaved plan edits.");
322 if (ImGui::Button(
"Discard and reload")) {
324 ImGui::CloseCurrentPopup();
327 if (ImGui::Button(
"Keep editing")) {
328 ImGui::CloseCurrentPopup();
339 return plan_.has_value();
346 if (!
plan_->title.empty()) {
347 ImGui::TextUnformatted(
plan_->title.c_str());
349 if (!
plan_->about.empty()) {
350 ImGui::TextWrapped(
"%s",
plan_->about.c_str());
362 auto& goal =
plan_->goal;
364 ImGui::SetNextItemWidth(-1);
365 if (ImGui::InputTextWithHint(
"##goal_title",
"What this push delivers",
369 ImGui::SetNextItemWidth(120);
370 if (ImGui::InputTextWithHint(
"##goal_due",
"YYYY-MM-DD", &goal.due)) {
373 const std::string left = DaysUntil(goal.due);
376 ImGui::TextDisabled(
"%s", left.c_str());
378 if (ImGui::InputTextMultiline(
"##goal_detail", &goal.detail,
379 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
389 if (!current->
detail.empty()) {
390 ImGui::TextWrapped(
"%s", current->
detail.c_str());
393 if (!ImGui::TreeNode(
"##milestones",
"All milestones (%zu)",
394 plan_->milestones.size())) {
397 if (ImGui::BeginTable(
"##milestone_table", 4,
398 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
399 ImGuiTableFlags_SizingStretchProp)) {
400 ImGui::TableSetupColumn(
"Milestone", ImGuiTableColumnFlags_WidthStretch);
401 ImGui::TableSetupColumn(
"Start", ImGuiTableColumnFlags_WidthFixed, 90);
402 ImGui::TableSetupColumn(
"End", ImGuiTableColumnFlags_WidthFixed, 90);
403 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthFixed, 110);
404 ImGui::TableHeadersRow();
405 for (
size_t i = 0; i <
plan_->milestones.size(); ++i) {
406 auto& m =
plan_->milestones[i];
407 ImGui::PushID(
static_cast<int>(i));
408 ImGui::TableNextRow();
409 ImGui::TableNextColumn();
410 ImGui::SetNextItemWidth(-1);
411 if (ImGui::InputText(
"##title", &m.title)) {
414 ImGui::TableNextColumn();
415 ImGui::SetNextItemWidth(-1);
416 if (ImGui::InputText(
"##start", &m.start)) {
419 ImGui::TableNextColumn();
420 ImGui::SetNextItemWidth(-1);
421 if (ImGui::InputText(
"##end", &m.end)) {
424 ImGui::TableNextColumn();
425 ImGui::SetNextItemWidth(-1);
426 if (StatusCombo(
"##status", &m.status, kMilestoneStatuses)) {
433 if (ImGui::SmallButton(
ICON_MD_ADD " Milestone")) {
435 m.
id =
plan_->NewMilestoneId();
436 m.
title =
"New milestone";
438 plan_->milestones.push_back(std::move(m));
446 if (
plan_->route_steps.empty()) {
447 ImGui::TextDisabled(
"No route steps yet. Add them in the Route window.");
450 ImGui::BeginChild(
"##route_strip",
451 ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 1.6f),
false,
452 ImGuiWindowFlags_HorizontalScrollbar);
453 for (
size_t i = 0; i <
plan_->route_steps.size(); ++i) {
454 const auto& step =
plan_->route_steps[i];
456 ImGui::SameLine(0, 2);
458 ImGui::SameLine(0, 2);
460 ImGui::PushID(
static_cast<int>(i));
463 ImGui::PushStyleColor(ImGuiCol_Button,
464 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
466 if (ImGui::Button(StepLabel(step).c_str())) {
471 ImGui::PopStyleColor();
473 if (ImGui::IsItemHovered()) {
474 ImGui::SetTooltip(
"%s\n%s", step.title.c_str(), step.note.c_str());
482 ImGui::Text(
"%s", step.title.c_str());
483 if (!step.note.empty()) {
484 ImGui::TextWrapped(
"%s", step.note.c_str());
488 for (
const auto* item :
plan_->ItemsAtStep(step.id)) {
489 ImGui::BulletText(
"%s", item->name.c_str());
497 const auto open =
plan_->OpenDecisions();
498 ImGui::SeparatorText(
501 for (
size_t i = 0; i <
plan_->decisions.size(); ++i) {
502 auto& d =
plan_->decisions[i];
503 ImGui::PushID(
static_cast<int>(i));
504 const bool is_open = d.status.empty() || d.status ==
"open";
505 std::string header = d.priority > 0
506 ? absl::StrFormat(
"%d. %s", d.priority, d.title)
509 header += absl::StrFormat(
" (%s)", d.status);
511 ImGui::PushStyleColor(ImGuiCol_Text,
512 is_open ? ImGui::GetStyleColorVec4(ImGuiCol_Text)
514 const bool expanded = ImGui::TreeNode(
"##decision",
"%s", header.c_str());
515 ImGui::PopStyleColor();
517 if (!d.recommendation.empty()) {
518 ImGui::TextWrapped(
"Recommendation: %s", d.recommendation.c_str());
520 ImGui::SetNextItemWidth(120);
521 if (StatusCombo(
"Status", &d.status, kDecisionStatuses)) {
524 if (ImGui::InputTextMultiline(
525 "##answer", &d.answer,
526 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
530 for (
const auto& ref : d.refs) {
531 ImGui::TextDisabled(
"%s", ref.c_str());
541 d.
title =
"New decision";
543 plan_->decisions.push_back(std::move(d));
549 if (
plan_->references.empty()) {
553 for (
size_t i = 0; i <
plan_->references.size(); ++i) {
554 const auto& r =
plan_->references[i];
555 ImGui::PushID(
static_cast<int>(i));
556 ImGui::BulletText(
"%s", r.label.c_str());
557 if (!r.author.empty() || !r.date.empty()) {
559 ImGui::TextDisabled(
"%s %s", r.author.c_str(), r.date.c_str());
561 if (!r.path.empty()) {
564 ImGui::SetClipboardText(r.path.c_str());
566 if (ImGui::IsItemHovered()) {
567 ImGui::SetTooltip(
"Copy path: %s", r.path.c_str());
578 if (ImGui::BeginTable(
580 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
581 ImGui::TableSetupColumn(
"Steps", ImGuiTableColumnFlags_WidthStretch, 0.45f);
582 ImGui::TableSetupColumn(
"Detail", ImGuiTableColumnFlags_WidthStretch,
584 ImGui::TableNextRow();
585 ImGui::TableNextColumn();
587 ImGui::TableNextColumn();
596 auto& steps =
plan_->route_steps;
597 ImGui::BeginChild(
"##step_list", ImVec2(0, 320));
598 std::string last_group;
601 for (
size_t i = 0; i < steps.size(); ++i) {
602 const auto& step = steps[i];
603 if (!step.group.empty() && step.group != last_group) {
604 ImGui::SeparatorText(step.group.c_str());
605 last_group = step.group;
607 ImGui::PushID(
static_cast<int>(i));
608 if (ImGui::Selectable(absl::StrFormat(
"%zu. %s %s", i + 1, StepLabel(step),
609 step.code.empty() ?
"" : step.title)
614 if (ImGui::BeginDragDropSource()) {
615 const int index =
static_cast<int>(i);
616 ImGui::SetDragDropPayload(
"PLAN_STEP", &index,
sizeof(index));
617 ImGui::TextUnformatted(StepLabel(step).c_str());
618 ImGui::EndDragDropSource();
620 if (ImGui::BeginDragDropTarget()) {
621 if (
const ImGuiPayload* payload =
622 ImGui::AcceptDragDropPayload(
"PLAN_STEP")) {
623 move_from = *
static_cast<const int*
>(payload->Data);
624 move_to =
static_cast<int>(i);
626 ImGui::EndDragDropTarget();
631 if (move_from >= 0 && move_to >= 0 && move_from != move_to) {
632 auto moved = std::move(steps[move_from]);
633 steps.erase(steps.begin() + move_from);
634 steps.insert(steps.begin() + move_to, std::move(moved));
641 step.
title =
"New step";
644 steps.insert(steps.begin() + at, std::move(step));
649 ImGui::TextDisabled(
"Drag steps to reorder");
653 auto& steps =
plan_->route_steps;
654 if (selected_step_ < 0 || selected_step_ >=
static_cast<int>(steps.size())) {
655 ImGui::TextDisabled(
"Select a step");
659 ImGui::PushID(
"step_detail");
660 std::string edited_id = step.id;
661 if (ImGui::InputText(
"Id", &edited_id)) {
662 const auto status =
plan_->RenameStep(step.id, std::move(edited_id));
670 if (ImGui::InputText(
"Code", &step.code)) {
673 if (ImGui::InputText(
"Title", &step.title)) {
676 if (ImGui::InputText(
"Group", &step.group)) {
679 if (StatusCombo(
"Kind", &step.kind, kStepKinds)) {
682 std::string targets = JoinTargets(step.targets);
683 if (ImGui::InputTextWithHint(
"Targets",
"room:0x0C, map:0x4B, area:D1",
685 step.targets = SplitTargets(targets);
689 if (ImGui::InputTextMultiline(
"##note", &step.note,
690 ImVec2(-1, ImGui::GetTextLineHeight() * 4))) {
697 std::min<int>(
selected_step_,
static_cast<int>(steps.size()) - 1);
704 auto& items =
plan_->route_items;
705 const std::string step_id =
707 selected_step_ < static_cast<int>(
plan_->route_steps.size())
710 ImGui::Text(
"Items (%zu)", items.size());
717 item.
name =
"New item";
720 items.push_back(std::move(item));
724 if (ImGui::BeginTable(
"##items", 5,
725 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
726 ImGuiTableFlags_BordersInnerH,
728 ImGui::TableSetupScrollFreeze(0, 1);
729 ImGui::TableSetupColumn(
"Item", ImGuiTableColumnFlags_WidthStretch);
730 ImGui::TableSetupColumn(
"Step", ImGuiTableColumnFlags_WidthFixed, 80);
731 ImGui::TableSetupColumn(
"Category", ImGuiTableColumnFlags_WidthFixed, 80);
732 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthFixed, 90);
733 ImGui::TableSetupColumn(
"Where", ImGuiTableColumnFlags_WidthStretch);
734 ImGui::TableHeadersRow();
735 for (
size_t i = 0; i < items.size(); ++i) {
736 const auto& item = items[i];
740 ImGui::PushID(
static_cast<int>(i));
741 ImGui::TableNextRow();
742 ImGui::TableNextColumn();
743 if (ImGui::Selectable(item.name.c_str(),
745 ImGuiSelectableFlags_SpanAllColumns)) {
748 ImGui::TableNextColumn();
749 const int step_index =
plan_->FindStep(item.step);
750 ImGui::TextUnformatted(
751 step_index >= 0 ? StepLabel(
plan_->route_steps[step_index]).c_str()
752 : item.step.c_str());
753 ImGui::TableNextColumn();
754 ImGui::TextUnformatted(item.category.c_str());
755 ImGui::TableNextColumn();
757 ImGui::TableNextColumn();
758 ImGui::TextUnformatted(item.where.c_str());
764 if (selected_item_ < 0 || selected_item_ >=
static_cast<int>(items.size())) {
768 ImGui::SeparatorText(item.name.c_str());
769 ImGui::PushID(
"item_detail");
770 if (ImGui::InputText(
"Name", &item.name)) {
773 if (ImGui::BeginCombo(
"Step", item.step.c_str())) {
774 for (
const auto& step :
plan_->route_steps) {
775 if (ImGui::Selectable(
776 absl::StrFormat(
"%s %s", StepLabel(step), step.title).c_str(),
777 item.step == step.id)) {
784 if (ImGui::InputText(
"Category", &item.category)) {
787 if (StatusCombo(
"Status", &item.status, kItemStatuses)) {
790 if (ImGui::Checkbox(
"Required", &item.required)) {
793 if (ImGui::InputText(
"Where", &item.where)) {
796 if (ImGui::InputTextMultiline(
"##item_note", &item.note,
797 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
801 for (
const auto& need : item.needs) {
802 const int step_index =
plan_->FindStep(need.step);
803 ImGui::BulletText(
"Needed at %s: %s",
805 ? StepLabel(
plan_->route_steps[step_index]).c_str()
819 if (section ==
nullptr) {
823 const auto observations = section->find(
"observations");
825 observations != section->end() && observations->is_object()
826 ? observations->size()
828 ImGui::TextDisabled(
"%zu observations, updated %s by %s", count,
829 JsonString(*section,
"updated_utc",
"-").c_str(),
830 JsonString(*section,
"agent",
"-").c_str());
832 if (!orphans.empty()) {
834 "%zu observations name entries no longer in the plan: %s",
835 orphans.size(), absl::StrJoin(orphans,
", ").c_str());
838 !ImGui::TreeNode(
"##agent_observations",
"Observations (%zu)", count)) {
841 for (
auto it = observations->begin(); it != observations->end(); ++it) {
842 ImGui::BulletText(
"%s", it.key().c_str());
843 const std::string state = JsonString(it.value(),
"state",
"");
844 if (!state.empty()) {
854 if (obs ==
nullptr) {
857 ImGui::PushID(key.c_str());
859 const std::string state = JsonString(*obs,
"state",
"");
860 if (!state.empty()) {
863 for (
const char* field : {
"summary",
"next"}) {
864 const std::string text = JsonString(*obs, field,
"");
866 ImGui::TextWrapped(
"%s", text.c_str());
869 if (
auto evidence = obs->find(
"evidence");
870 evidence != obs->end() && evidence->is_array()) {
871 for (
const auto& entry : *evidence) {
872 if (entry.is_string()) {
873 ImGui::BulletText(
"%s", entry.get<std::string>().c_str());
877 if (
auto prs = obs->find(
"prs"); prs != obs->end() && prs->is_array()) {
878 std::vector<std::string> labels;
879 for (
const auto& pr : *prs) {
880 labels.push_back(pr.is_string() ? pr.get<std::string>()
883 ImGui::TextDisabled(
"PRs: %s", absl::StrJoin(labels,
", ").c_str());
886 "%s %s - read-only; agents update this, your notes stay "
888 JsonString(*obs,
"agent",
"").c_str(),
889 JsonString(*obs,
"observed_utc",
"").c_str());
895 if (targets.empty()) {
899 for (
size_t i = 0; i < targets.size(); ++i) {
903 ImGui::PushID(
static_cast<int>(i));
906 if (ImGui::SmallButton(targets[i].c_str())) {
910 ImGui::TextDisabled(
"%s", targets[i].c_str());
921 if (!bus ||
id < 0) {
924 if (target.
kind ==
"room") {
926 }
else if (target.
kind ==
"map") {
static absl::Status CreateAndLink(project::YazeProject *project, const std::string &path)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path, std::string *source=nullptr)
project::YazeProject * project() const
EditorDependencies dependencies_
absl::Status Reload(bool discard_changes=false)
std::string loaded_source_
void Initialize() override
absl::Status Save() override
std::string new_plan_path_
std::string project_path_
void DrawAgentObservation(const std::string &key)
void DrawTargets(const std::vector< std::string > &targets, const char *id)
void DiscardProjectDraftChanges()
std::optional< core::ProjectPlan > plan_
absl::Status SyncWithProject()
absl::Status Update() override
void OpenTarget(const std::string &target)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Base interface for all logical window content components.
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::function< void()> draw_
void Draw(bool *) override
Draw the panel content.
std::string GetId() const override
Unique identifier for this panel.
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
WindowPresentationPolicy GetPresentationPolicy() const override
How this content is hosted and advertised by workspace chrome.
bool IsVisibleByDefault() const override
Whether this panel should be visible by default.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
PlanningPanel(std::string id, std::string name, const char *icon, int priority, std::function< void()> draw)
int GetPriority() const override
Get display priority for menu ordering.
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
#define ICON_MD_EVENT_NOTE
#define ICON_MD_CHECKLIST
#define ICON_MD_DASHBOARD
#define ICON_MD_CONTENT_COPY
#define ICON_MD_CHEVRON_RIGHT
#define ICON_MD_SMART_TOY
const PlanJson * FindPlanAgentStatus(const ProjectPlan &plan)
const PlanJson * FindPlanAgentObservation(const ProjectPlan &plan, std::string_view key)
bool AdoptPlanAgentOnlyChanges(ProjectPlan *draft, std::string_view loaded_source, std::string_view disk_source)
nlohmann::ordered_json PlanJson
std::vector< std::string > OrphanedPlanAgentObservations(const ProjectPlan &plan)
::yaze::EventBus * event_bus()
Get the current EventBus instance.
bool StatusCombo(const char *label, std::string *value, const char *const (&options)[N])
std::vector< std::string > SplitTargets(const std::string &text)
ImVec4 StatusColor(const std::string &status)
constexpr const char * kDecisionStatuses[]
constexpr const char * kStepKinds[]
std::string StepLabel(const core::PlanRouteStep &step)
constexpr const char * kMilestoneStatuses[]
constexpr const char * kItemStatuses[]
std::string JoinTargets(const std::vector< std::string > &targets)
std::string JsonString(const core::PlanJson &obj, const char *key, const char *fallback)
std::string DaysUntil(const std::string &date)
constexpr const char * kCategory
Editors are the view controllers for the application.
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetDisabledColor()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
ImVec4 GetModifiedColor()
static PlanTarget Parse(std::string_view text)
ToastManager * toast_manager
WorkspaceWindowManager * window_manager
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
std::string PlanningFilePath() const