diff --git a/AGENTS.md b/AGENTS.md index 443ee7c..19e3658 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,13 @@ 除非特别说明不允许写任何同步等待代码。 `` +`` +除非特别说明不允许写任何 快照, 任何线程问题先完了都要给我说明 我来审核。 +除非特别说明不允许写任何 快照, 任何线程问题先完了都要给我说明 我来审核。 +除非特别说明不允许写任何 快照, 任何线程问题先完了都要给我说明 我来审核。 +除非特别说明不允许写任何 快照, 任何线程问题先完了都要给我说明 我来审核。 +`` + 我使用 里的这个环境 D:\ae\proj\Aethera\CMakePresets.json "toolchain/vs2022.json" ====================[ 构建 | Aethera_Kernel_check | vs2022_debug ]================ "C:\Program Files\JetBrains\CLion 2026.1\bin\cmake\win\x64\bin\cmake.exe" --build D: diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp index 73b4580..a2c6b52 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -61,10 +61,16 @@ public: return {{"success", false}, {"error", "unknown component"}}; return (*found)->state(); } - nlohmann::json component_snapshots() const override { + nlohmann::json capture_components( + const std::vector& components) const override { nlohmann::json result = nlohmann::json::object(); - for (const auto& descriptor : descriptors) - result[std::string(descriptor->id())] = descriptor->snapshot(); + for (const auto& component : components) { + const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { + return item->id() == component; + }); + if (found != descriptors.end()) + result[component] = (*found)->snapshot(); + } return result; } nlohmann::json data_generator_schema() const override { diff --git a/web_server/src/Gallery_Plots_3D.cpp b/web_server/src/Gallery_Plots_3D.cpp index 99ce7c5..d5e6f65 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/web_server/src/Gallery_Plots_3D.cpp @@ -303,10 +303,16 @@ public: return {{"success", false}, {"error", "unknown component"}}; return (*found)->state(); } - [[nodiscard]] nlohmann::json component_snapshots() const override { + [[nodiscard]] nlohmann::json capture_components( + const std::vector& components) const override { nlohmann::json result = nlohmann::json::object(); - for (const auto& descriptor : descriptors_) - result[std::string(descriptor->id())] = descriptor->snapshot(); + for (const auto& component : components) { + const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) { + return descriptor->id() == component; + }); + if (found != descriptors_.end()) + result[component] = (*found)->snapshot(); + } return result; } [[nodiscard]] nlohmann::json data_generator_schema() const override { diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 2034f80..72ea63c 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -19,11 +19,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -200,7 +200,7 @@ void append_event_statistics_json(nlohmann::json& output, nlohmann::json taskflow_trace_json( const Taskflow_Frame_Trace& trace, - const nlohmann::json& component_snapshots) { + const nlohmann::json& captured_components) { nlohmann::json markers = nlohmann::json::object(); for (const auto& marker : trace.markers) markers[magic_enum::enum_name(marker.marker)] = @@ -228,14 +228,14 @@ nlohmann::json taskflow_trace_json( {"attributes", std::move(attributes)}}; const auto owner = encoded["attributes"].value( "owner_component", std::string{}); - if (!owner.empty() && component_snapshots.contains(owner)) { - const auto& snapshot = component_snapshots.at(owner); + if (!owner.empty() && captured_components.contains(owner)) { + const auto& captured = captured_components.at(owner); encoded["owner"] = { {"component", owner}, - {"label", snapshot.value("label", owner)}, - {"kind", snapshot.value("kind", std::string{})}}; - encoded["prop"] = snapshot.value("prop", nlohmann::json::object()); - encoded["state"] = snapshot.value("state", nlohmann::json::object()); + {"label", captured.value("label", owner)}, + {"kind", captured.value("kind", std::string{})}}; + encoded["prop"] = captured.value("prop", nlohmann::json::object()); + encoded["state"] = captured.value("state", nlohmann::json::object()); } nodes.push_back(std::move(encoded)); } @@ -343,6 +343,10 @@ struct Plot::Private { std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */ Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */ std::atomic state{Frame_State::available}; /* 本槽唯一生命周期状态。 */ + std::atomic_size_t diagnostic_readers{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */ + std::uint64_t statistics_generation{}; /* 与本槽中完成帧统计共同发布。 */ + Frame_Statistics_State statistics{}; /* 统计结果归属当前完成帧,不在 Plot 复制。 */ + std::atomic retired_next{}; /* 无锁退役队列的槽内侵入链接。 */ }; struct Consumer { @@ -370,6 +374,9 @@ struct Plot::Private { Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */ static constexpr std::size_t scene_frame_capacity{3}; std::array frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */ + std::atomic latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */ + std::atomic retired_frames{}; /* 多回调生产、唯一 Taskflow 任务消费。 */ + std::atomic_bool retired_frame_task_scheduled{}; Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */ std::atomic> pending_tick{}; std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */ @@ -401,8 +408,8 @@ struct Plot::Private { std::atomic_bool post_publish_busy{}; std::vector> completion_extensions{}; /* 生命周期覆盖 post-publish module 借用。 */ Frame_Statistics_Accumulator completed_frame_statistics{diagnostic_window_capacity}; - Frame_Statistics_State completed_frame_statistics_state{}; - mutable std::mutex completed_frame_statistics_mutex{}; + std::uint64_t applied_statistics_generation{}; /* 仅完成帧退役任务读写。 */ + std::atomic_uint64_t statistics_generation{}; /* reset 只推进代次,不触碰单写者累加器。 */ template Private(std::unique_ptr value_scene, @@ -438,6 +445,9 @@ struct Plot::Private { void publish_completed_frame(); void consume_completed_frame(Render_Frame* frame); void retire_completed_frame(Render_Frame* frame); + void consume_retired_frames(); + void finalize_retired_frame(Render_Frame* frame); + void arm_retired_frame_consumer(); void attach_completion(std::unique_ptr completion); [[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame); [[nodiscard]] bool mark_post_publish_taskflow_trace(); @@ -445,7 +455,7 @@ struct Plot::Private { std::array>, maximum_taskflow_trace_frames>& slots, const Taskflow_Frame_Trace& trace, - const nlohmann::json& component_snapshots = {}); + const nlohmann::json& captured_components = {}); [[nodiscard]] nlohmann::json trace_response( const std::atomic_uint64_t& control, const std::atomic_size_t& remaining, @@ -628,9 +638,9 @@ void Plot::Private::store_trace( std::array>, maximum_taskflow_trace_frames>& slots, const Taskflow_Frame_Trace& value, - const nlohmann::json& component_snapshots) { + const nlohmann::json& captured_components) { auto trace = std::make_shared( - taskflow_trace_json(value, component_snapshots)); + taskflow_trace_json(value, captured_components)); auto state = control.load(std::memory_order_acquire); for (;;) { const auto requested = static_cast(state >> 32U); @@ -690,11 +700,32 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { * 诊断生命周期。只要还有 available 槽,下一帧即可进入。 */ for (std::size_t index = 0; index < frame_slots.size(); ++index) { - auto expected = Frame_State::available; - if (!frame_slots[index].state.compare_exchange_strong( - expected, Frame_State::rendering, - std::memory_order_acq_rel, std::memory_order_acquire)) + auto* candidate = &frame_slots[index]; + auto* published = candidate; + const bool was_latest = latest_statistics_frame.compare_exchange_strong( + published, nullptr, std::memory_order_acq_rel, + std::memory_order_acquire); + if (candidate->diagnostic_readers.load(std::memory_order_acquire) != 0) { + if (was_latest) { + Managed_Frame* empty{}; + static_cast(latest_statistics_frame.compare_exchange_strong( + empty, candidate, std::memory_order_release, + std::memory_order_relaxed)); + } continue; + } + auto expected = Frame_State::available; + if (!candidate->state.compare_exchange_strong( + expected, Frame_State::rendering, + std::memory_order_acq_rel, std::memory_order_acquire)) { + if (was_latest) { + Managed_Frame* empty{}; + static_cast(latest_statistics_frame.compare_exchange_strong( + empty, candidate, std::memory_order_release, + std::memory_order_relaxed)); + } + continue; + } slot_index = index; managed = &frame_slots[index]; break; @@ -961,6 +992,46 @@ void Plot::Private::consume_completed_frame(Render_Frame* frame) { } } +void Plot::Private::arm_retired_frame_consumer() { + bool expected{}; + if (!retired_frame_task_scheduled.compare_exchange_strong( + expected, true, std::memory_order_acq_rel, + std::memory_order_acquire)) return; + const auto weak = lifetime; + schedule_task("web.plot.frame.retire", [weak] { + if (const auto owner = weak.lock()) + owner->d->consume_retired_frames(); + }); +} + +void Plot::Private::consume_retired_frames() { + for (;;) { + auto* list = retired_frames.exchange(nullptr, std::memory_order_acq_rel); + if (!list) break; + std::vector frames; + while (list) { + auto* next = list->retired_next.exchange( + nullptr, std::memory_order_relaxed); + frames.push_back(list); + list = next; + } + std::ranges::sort(frames, {}, [](const Managed_Frame* managed) { + return std::visit( + [](const auto& value) { return value->identity().sequence; }, + managed->frame); + }); + for (auto* managed : frames) { + auto* frame = std::visit( + [](const auto& value) -> Render_Frame* { return value.get(); }, + managed->frame); + finalize_retired_frame(frame); + } + } + retired_frame_task_scheduled.store(false, std::memory_order_release); + if (retired_frames.load(std::memory_order_acquire)) + arm_retired_frame_consumer(); +} + void Plot::Private::retire_completed_frame(Render_Frame* frame) { if (!frame) throw std::invalid_argument("Plot received a null retired frame"); @@ -976,23 +1047,82 @@ void Plot::Private::retire_completed_frame(Render_Frame* frame) { if (!managed) throw std::logic_error("retired frame has no owned Plot slot"); - { - std::lock_guard lock(completed_frame_statistics_mutex); - completed_frame_statistics_state = completed_frame_statistics.submit(*frame); - } + auto* head = retired_frames.load(std::memory_order_relaxed); + do { + managed->retired_next.store(head, std::memory_order_relaxed); + } while (!retired_frames.compare_exchange_weak( + head, managed, std::memory_order_release, + std::memory_order_relaxed)); + arm_retired_frame_consumer(); +} - if (frame->taskflow_trace_requested()) - store_trace(taskflow_trace_control, taskflow_trace_slots, - frame->take_taskflow_trace(), view->component_snapshots()); +void Plot::Private::finalize_retired_frame(Render_Frame* frame) { + Managed_Frame* managed{}; + for (auto& slot : frame_slots) { + auto* address = std::visit( + [](const auto& value) -> Render_Frame* { return value.get(); }, + slot.frame); + if (address == frame) { + managed = &slot; + break; + } + } + if (!managed) + throw std::logic_error("retired frame has no owned Plot slot"); + + const auto statistics_generation_value = + statistics_generation.load(std::memory_order_acquire); + if (applied_statistics_generation != statistics_generation_value) { + completed_frame_statistics.reset(); + applied_statistics_generation = statistics_generation_value; + } + managed->statistics = completed_frame_statistics.submit(*frame); + managed->statistics_generation = statistics_generation_value; + + std::optional captured_trace; + nlohmann::json captured_components; + if (frame->taskflow_trace_requested()) { + captured_trace.emplace(frame->take_taskflow_trace()); + std::unordered_set executed_nodes; + for (const auto& execution : captured_trace->tasks) + executed_nodes.insert(execution.native_id); + std::vector executed_components; + for (const auto& graph : captured_trace->graphs) { + for (const auto& node : graph.nodes) { + if (!executed_nodes.contains(node.native_id)) continue; + const auto owner = std::ranges::find( + node.attributes, "owner_component", + &std::pair::first); + if (owner == node.attributes.end() || owner->second.empty() || + std::ranges::find(executed_components, owner->second) != + executed_components.end()) continue; + executed_components.push_back(owner->second); + } + } + captured_components = view->capture_components(executed_components); + } auto expected = Frame_State::consuming; if (!managed->state.compare_exchange_strong( expected, Frame_State::available, std::memory_order_acq_rel, std::memory_order_acquire)) throw std::logic_error("retired Plot frame is not consuming"); + latest_statistics_frame.store(managed, std::memory_order_release); /* 三个消费者槽曾全部占满时,退役一个槽后继续 latest pending。 */ arm_tick_consumer(lifetime); + + if (captured_trace) { + auto owner = lifetime; + schedule_task("plot.taskflow.serialize", [owner, + trace = std::move(*captured_trace), + components = std::move(captured_components)]() mutable { + if (const auto plot = owner.lock()) + plot->d->store_trace( + plot->d->taskflow_trace_control, + plot->d->taskflow_trace_slots, trace, components); + }); + } } void Plot::Private::attach_completion( @@ -1221,8 +1351,23 @@ nlohmann::json Plot::diagnostics() const { }, d->scene); { - std::lock_guard lock(d->completed_frame_statistics_mutex); - const auto statistics = d->completed_frame_statistics_state; + const auto generation = + d->statistics_generation.load(std::memory_order_acquire); + auto* completed_frame = + d->latest_statistics_frame.load(std::memory_order_acquire); + Frame_Statistics_State statistics{}; + if (completed_frame) { + completed_frame->diagnostic_readers.fetch_add( + 1, std::memory_order_acq_rel); + if (d->latest_statistics_frame.load(std::memory_order_acquire) == + completed_frame && + completed_frame->state.load(std::memory_order_acquire) == + Private::Frame_State::available && + completed_frame->statistics_generation == generation) + statistics = completed_frame->statistics; + completed_frame->diagnostic_readers.fetch_sub( + 1, std::memory_order_release); + } append_statistic_json(frame_statistics, statistics); identity = statistics.identity; created_time_unix_ns = statistics.created_time_unix_ns; @@ -1375,8 +1520,6 @@ nlohmann::json Plot::post_publish_taskflow_trace() const { void Plot::reset_diagnostics() { std::visit([](auto& scene) { scene->reset_diagnostics(); }, d->scene); - std::lock_guard lock(d->completed_frame_statistics_mutex); - d->completed_frame_statistics.reset(); - d->completed_frame_statistics_state = {}; + d->statistics_generation.fetch_add(1, std::memory_order_acq_rel); } } diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index cb228e7..2969584 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -67,7 +67,8 @@ public: const nlohmann::json& value) = 0; [[nodiscard]] virtual nlohmann::json component_state( std::string_view component) const = 0; - [[nodiscard]] virtual nlohmann::json component_snapshots() const = 0; + [[nodiscard]] virtual nlohmann::json capture_components( + const std::vector& components) const = 0; [[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0; [[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0; virtual void update(const Plot_Render_Tick& tick) = 0; diff --git a/web_server/src/Taskflow_Trace_Json.hpp b/web_server/src/Taskflow_Trace_Json.hpp index 923decf..83b6da0 100644 --- a/web_server/src/Taskflow_Trace_Json.hpp +++ b/web_server/src/Taskflow_Trace_Json.hpp @@ -5,5 +5,5 @@ namespace aethera::web { [[nodiscard]] nlohmann::json taskflow_trace_json( const Taskflow_Frame_Trace& trace, - const nlohmann::json& component_snapshots = {}); + const nlohmann::json& captured_components = {}); } diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 99ab425..eab2014 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -1146,9 +1146,25 @@ function taskflow_node_phase(name: string): Taskflow_Node_Phase { return "other"; } -function taskflow_node_name(name: string) { +function taskflow_node_name(name: string, node?: Taskflow_Node_Trace) { const parts = name.split(".").filter(Boolean); if (!parts.length) return name; + const owner = node?.owner?.label || parts[0]; + + const local_labels: Record = { + "scene.frame_target": "Scene · 清空最终帧画布", + frame_target: "Scene · 清空最终帧画布", + "scene.background": "Scene · 填充背景色", + background: "Scene · 填充背景色", + "scene.cache_targets": "Scene · 准备需要重绘的缓存画布", + cache_targets: "Scene · 准备需要重绘的缓存画布", + "scene.render.ready": "Scene · 等待画布准备完成", + ready: "Scene · 等待画布准备完成", + prepare: `${owner} · 准备本帧绘制数据`, + "paint.anchors": `${owner} · 绘制理想参考锚点`, + "paint.partition": `${owner} · 绘制数据分区` + }; + if (local_labels[name]) return local_labels[name]; if (name === "scene.begin") return "Scene · 帧开始"; if (name === "scene.prepare") return "Scene · 数据准备子图"; @@ -1173,7 +1189,6 @@ function taskflow_node_name(name: string) { if (name === "taskflow.observer.deactivate_graph") return "Taskflow · Observer graph 退役"; if (name === "taskflow.graph.finish") return "Taskflow · Graph 完成记录"; - const owner = parts[0]; const stage = parts[1]; const operation = parts[2] ?? parts[1]; if (stage === "prepare") { @@ -1257,7 +1272,7 @@ function Taskflow_Node_Label({node, sample, summary, state, level}: { onPointerDown={event => event.stopPropagation()} onWheel={event => event.stopPropagation()}>
- {taskflow_node_name(node.name)} + {taskflow_node_name(node.name, node)} {phase_label ? {phase_label} : null}
@@ -1882,6 +1897,8 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful const [visible_ranges, set_visible_ranges] = useState>({}); const [type_visibility, set_type_visibility] = useState>({condition: false}); const [hot_only, set_hot_only] = useState(false); + const [hot_threshold_percent, set_hot_threshold_percent] = useState(10); + const [selected_node_id, set_selected_node_id] = useState(""); const node_types = useMemo(() => [...new Set(graph.nodes.map(node => node.type.toLowerCase()))].sort(), [graph]); const fullscreen = controlled_fullscreen ?? local_fullscreen; const set_fullscreen = (value: boolean) => on_fullscreen_change ? on_fullscreen_change(value) : set_local_fullscreen(value); @@ -1895,7 +1912,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful const rows = candidate_rows.filter(execution => { const type = node_by_native_id.get(execution.native_id)?.type.toLowerCase() ?? "unknown"; if (!(type_visibility[type] ?? type !== "condition")) return false; - return !hot_only || execution.duration_ms >= maximum_duration * .1; + return !hot_only || execution.duration_ms >= maximum_duration * hot_threshold_percent / 100; }).sort((left, right) => left.started_ms - right.started_ms || left.worker_id - right.worker_id); const groups: Taskflow_Timeline_Group[] = [{ id: "topology", title:
Topology @@ -1934,18 +1951,20 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful const diagnostic = node?.type.toLowerCase() === "diagnostic"; groups.push({id: group, node_name, worker_id: diagnostic ? undefined : execution.worker_id, started_ms: execution.started_ms, type: node?.type, - title:
- {taskflow_node_name(node_name)} + title:
}); + }); const detail = diagnostic ? `${node_name} · Runtime tail` : `${node_name} · Worker ${execution.worker_id}`; const diagnostic_detail = detail + (node?.owner ? `\nowner: ${node.owner.label} (${node.owner.component})\nprop: ${JSON.stringify(node.prop ?? {})}\nstate: ${JSON.stringify(node.state ?? {})}` : ""); if (diagnostic) { add_item(`${group}-runtime`, group, "completion_tail", execution.started_ms, - execution.finished_ms, `${taskflow_node_name(node_name)} ${milliseconds(execution.duration_ms)}`, + execution.finished_ms, `${taskflow_node_name(node_name, node)} ${milliseconds(execution.duration_ms)}`, node_name, diagnostic_detail); return; } @@ -1966,7 +1985,8 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful return {groups, items, end: origin + extent * 1.06 * scale, span: Math.max(.1, extent * 1.06) * scale, executions: rows.length, last_completed, last_task_completed, snapshots}; - }, [graph, executions, type_visibility, hot_only]); + }, [graph, executions, type_visibility, hot_only, hot_threshold_percent, selected_node_id]); + const selected_node = graph.nodes.find(node => node.id === selected_node_id); const timeline_view_key = `${graph.stage}:${graph.name}`; const visible_range = visible_ranges[timeline_view_key]; const timeline_time_props = visible_range @@ -2019,10 +2039,24 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful {node_types.map(type => )} - + +
+ {selected_node ?
+
{taskflow_node_name(selected_node.name, selected_node)} + {selected_node.owner ? `${selected_node.owner.label} · ${selected_node.owner.component}` : "无组件归属"}
+
+

任务说明

{selected_node.name}

{selected_node.id}
+

捕获时属性 Prop

{JSON.stringify(selected_node.prop ?? {}, null, 2)}
+

捕获时状态 State

{JSON.stringify(selected_node.state ?? {}, null, 2)}
+
:
点击左侧任务名称,可查看该节点捕获时的 Prop / State。
} {model.snapshots.length ?
时间线节点属性 / 状态({model.snapshots.length}) -
{model.snapshots.map(node =>
{taskflow_node_name(node.name)} +
{model.snapshots.map(node =>
{taskflow_node_name(node.name, node)} {node.owner?.label} · {node.owner?.component}
{JSON.stringify({prop: node.prop ?? {}, state: node.state ?? {}}, null, 2)}
)}
: null} @@ -2213,7 +2247,7 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon {graph_analysis.diagnostic_rows.map(row => { const node = graph.nodes.find(value => value.native_id === row.native_id); return
-
{taskflow_node_name(node?.name ?? row.node_id)}
{milliseconds(row.duration_ms)}
; +
{taskflow_node_name(node?.name ?? row.node_id, node)}
{milliseconds(row.duration_ms)}
; })}
入口队列峰值
{graph_analysis.peak_entry_queue}
实际最大并行
{graph_analysis.maximum_parallelism}
diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index 8b32a00..f864614 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -176,6 +176,22 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; } .taskflowTimelineFilters { display: flex; flex-wrap: wrap; gap: 8px 14px; padding: 9px 11px; color: #91a5c0; border: 1px solid #29435e; border-radius: 8px; background: #0c1a2a; font-size: 11px; } .taskflowTimelineFilters label { display: inline-flex; align-items: center; gap: 5px; } .taskflowTimelineFilters input { accent-color: #5ce4c2; } +.taskflowHotThreshold input[type="range"] { width: 110px; } +.taskflowHotThreshold input[type="number"] { width: 54px; padding: 3px 5px; color: #dce8f8; border: 1px solid #29435e; border-radius: 5px; background: #08111e; } +.taskflowTimelineSelectionHint { padding: 8px 11px; color: #7189a8; border-left: 2px solid #29435e; font-size: 11px; } +.taskflowSelectedCapture { overflow: hidden; border: 1px solid #37607f; border-radius: 9px; background: #091321; } +.taskflowSelectedCapture > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border-bottom: 1px solid #203651; background: #0d1b2c; } +.taskflowSelectedCapture > header > div { display: grid; gap: 3px; } +.taskflowSelectedCapture > header strong { color: #dce8f8; } +.taskflowSelectedCapture > header span { color: #7189a8; font-size: 10px; } +.taskflowSelectedCapture > header button { padding: 5px 9px; color: #9db1cb; border: 1px solid #29435e; border-radius: 6px; background: #08111e; cursor: pointer; } +.taskflowSelectedCapture > div { display: grid; grid-template-columns: minmax(180px, .7fr) repeat(2, minmax(240px, 1fr)); } +.taskflowSelectedCapture article { min-width: 0; padding: 10px 11px; border-right: 1px solid #203651; } +.taskflowSelectedCapture article:last-child { border-right: 0; } +.taskflowSelectedCapture h4 { margin: 0 0 7px; color: #8fa7c4; font-size: 10px; font-weight: 600; } +.taskflowSelectedCapture p { margin: 0 0 6px; color: #dce8f8; font: 11px/1.4 ui-monospace, monospace; } +.taskflowSelectedCapture code { color: #6f87a6; font: 9px/1.4 ui-monospace, monospace; overflow-wrap: anywhere; } +.taskflowSelectedCapture pre { max-height: 210px; overflow: auto; margin: 0; color: #b9cce3; font: 10px/1.45 ui-monospace, monospace; } .taskflowTimelineSnapshots { padding: 9px 11px; color: #91a5c0; border: 1px solid #29435e; border-radius: 8px; background: #091321; } .taskflowTimelineSnapshots > summary { cursor: pointer; } .taskflowTimelineSnapshots > div { display: grid; gap: 8px; margin-top: 9px; } @@ -262,9 +278,12 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; } .taskflowTimelineSection { min-width: 760px; overflow: visible; border: 1px solid #213653; border-radius: 10px; background: #07101c; } .taskflowTimelineViewport { max-height: 760px; overflow: auto; scrollbar-gutter: stable; } .taskflowTimelineViewport > .react-calendar-timeline { min-width: 900px; color: #b8cbe2; font-family: ui-monospace, monospace; } -.taskflowTimelineGroupTitle { display: grid; align-content: center; min-width: 0; height: 100%; padding: 4px 8px; gap: 3px; white-space: normal; overflow-wrap: anywhere; } +.taskflowTimelineGroupTitle { display: grid; align-content: center; width: 100%; min-width: 0; height: 100%; padding: 4px 8px; gap: 3px; color: inherit; border: 0; border-left: 2px solid transparent; background: transparent; text-align: left; white-space: normal; overflow-wrap: anywhere; cursor: pointer; } +.taskflowTimelineGroupTitle:hover { background: #13243a; } +.taskflowTimelineGroupTitle.active { border-left-color: #5ce4c2; background: #132b3d; } .taskflowTimelineGroupTitle strong { color: #dce8f8; font: 10px/1.2 ui-monospace, monospace; } .taskflowTimelineGroupTitle span { color: #7189a8; font: 8px/1.2 ui-monospace, monospace; } +@media (max-width: 900px) { .taskflowSelectedCapture > div { grid-template-columns: 1fr; } .taskflowSelectedCapture article { border-right: 0; border-bottom: 1px solid #203651; } } .taskflowTimelineSidebarHeader { display: flex; align-items: center; height: 100%; padding: 0 10px; color: #91a5c0; background: #101d2f; font: 10px/1.2 ui-monospace, monospace; } .taskflowTimelineViewport .rct-sidebar { border-color: #29405e; background: #091321; } .taskflowTimelineViewport .rct-sidebar .rct-sidebar-row { overflow: visible; border-color: #1d304a; }