diff --git a/SESSION.md b/SESSION.md index 89b6104..10a0f2b 100644 --- a/SESSION.md +++ b/SESSION.md @@ -716,12 +716,14 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - Latest E11 validation passed: 16 focused external-context/static tests, 730 full unit tests, 73 script static checks, `node --check src/web/app.js`, live deploy completed, remote ops smoke passed 9 checks, and remote E11 smoke confirmed capture surface summary/cards, external visual evidence chain entry, and deployed `Open Capture Surface` / `Open Capture Target` / `Open Capture Embed` UI strings on the live host. - Latest W8 validation passed: 69 focused workflow bridge/API/static tests, 731 full unit tests, 73 script static checks, `node --check src/web/app.js`, live deploy completed, remote ops smoke passed 9 checks, and remote W8 smoke confirmed handoff packet starter, replay note bridge summary, artifact bundle starter export, and deployed `Prefill Compare Replay Note` / `Export Handoff Starter` / `Export Artifact Bundle` UI strings on the live host. - Latest O2 validation passed: 74 focused archive/API/static/operator tests, 732 full unit tests, 73 script static checks, `node --check src/web/app.js`, live deploy completed, remote ops smoke passed 9 checks, and remote O2 smoke confirmed persisted digest/workflow baselines, long-range trend stories, and deployed `Save Baseline` / `Save Workflow Baseline` UI strings on the live host. +- Latest B7 validation passed: 74 focused archive/API/static/operator tests, 732 full unit tests, 73 script static checks, `node --check src/web/app.js`, live deploy completed, remote ops smoke passed 9 checks, and remote B7 smoke confirmed persisted archive defaults, cross-profile preset retrieval, and deployed `Save Digest Defaults` / `Use Saved Digest Defaults` / `Save Workflow Defaults` / `Use Saved Workflow Defaults` UI strings on the live host. +- Latest E12 validation passed: 16 focused external-context/static tests, 732 full unit tests, 73 script static checks, `node --check src/web/app.js`, live deploy completed, remote ops smoke passed 9 checks, and remote E12 smoke confirmed screenshot readiness summary, capture-surface screenshot status fields, and deployed `Open Screenshot Surface` UI strings on the live host. - Refreshed `docs/AI_ICT_??????????????????????2026-05-14_v1.md` after closing Phases A-E; the active remaining mainline backlog is now archive long-range trend deepening, archive deep-retrieval deepening, external context deep visual evidence extensions, and workflow archive deep bridge extensions. - Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter. ## Exact Next Step -Use `docs/AI_ICT_???????????????????2026-05-14_v1.md` as the completed enhancement record for Phases A-E. The active backlog source is now `docs/AI_ICT_??????????????????????2026-05-14_v1.md`. `B5 archive keyword search / export starter slice`, `W7 workflow archive replay / handoff / export bridge starter slice`, `E10 external context visual evidence / provider health starter slice`, `O1 archive long-range trend / baseline pin starter slice`, `B6 archive deep retrieval / structured export follow-up starter slice`, `E11 external context deep visual evidence / capture surface follow-up starter slice`, `W8 workflow archive deep bridge extension starter slice`, and `O2 archive long-range trend deepening / baseline persistence starter slice` are now closed. Start from `B7 archive deep retrieval deepening / preset persistence starter slice`, keep work on the analysis-assistant mainline only, and continue treating real-trading scope as deferred. PXX remains closed and you still must not create P25/P26 automatically or enable real trading. +Use `docs/AI_ICT_???????????????????2026-05-14_v1.md` as the completed enhancement record for Phases A-E. The active backlog source is now `docs/AI_ICT_??????????????????????2026-05-14_v1.md`. `B5 archive keyword search / export starter slice`, `W7 workflow archive replay / handoff / export bridge starter slice`, `E10 external context visual evidence / provider health starter slice`, `O1 archive long-range trend / baseline pin starter slice`, `B6 archive deep retrieval / structured export follow-up starter slice`, `E11 external context deep visual evidence / capture surface follow-up starter slice`, `W8 workflow archive deep bridge extension starter slice`, `O2 archive long-range trend deepening / baseline persistence starter slice`, `B7 archive deep retrieval deepening / preset persistence starter slice`, and `E12 external context visual evidence extension / screenshot readiness starter slice` are now closed. Start from `W9 workflow archive deep bridge extension / bundle export persistence slice`, keep work on the analysis-assistant mainline only, and continue treating real-trading scope as deferred. PXX remains closed and you still must not create P25/P26 automatically or enable real trading. ## Validation Commands Used Recently @@ -777,3 +779,5 @@ D:\ai-exchange\.venv\Scripts\python.exe -m unittest tests.unit.test_run_automati D:\ai-exchange\.venv\Scripts\python.exe scripts\run_automation_execution_closure_gate.py ``` + + diff --git a/TASKS.md b/TASKS.md index 68fbd3b..d4e0f52 100644 --- a/TASKS.md +++ b/TASKS.md @@ -111,7 +111,9 @@ - [x] E11 external context deep visual evidence / capture surface follow-up starter slice - [x] W8 workflow archive deep bridge extension starter slice - [x] O2 archive long-range trend deepening / baseline persistence starter slice -- [ ] B7 archive deep retrieval deepening / preset persistence starter slice +- [x] B7 archive deep retrieval deepening / preset persistence starter slice +- [x] E12 external context visual evidence extension / screenshot readiness starter slice +- [ ] W9 workflow archive deep bridge extension / bundle export persistence slice - [x] A1 independent preferences surface / dedicated settings section - [x] A2 preference profile create / duplicate / rename / delete @@ -845,3 +847,4 @@ - [ ] Long-term target: complete `Real-time Assisted Trading V1` without enabling default live auto-trading + diff --git a/docs/AI_ICT_需求文档剩余功能与延期项清单_2026-05-14_v1.md b/docs/AI_ICT_需求文档剩余功能与延期项清单_2026-05-14_v1.md index 4c07bfc..90ccef0 100644 --- a/docs/AI_ICT_需求文档剩余功能与延期项清单_2026-05-14_v1.md +++ b/docs/AI_ICT_需求文档剩余功能与延期项清单_2026-05-14_v1.md @@ -124,5 +124,27 @@ - 下一刀最小闭环切片改为: - `B7 archive deep retrieval deepening / preset persistence starter slice` +## 9. 2026-05-15 B7 Addendum + +- `B7 archive deep retrieval deepening / preset persistence starter slice` 已完成。 +- 当前已具备: + - archive query preset 持久化 starter fields + - cross-profile archive retrieval starter summary + - deeper structured export summary starter fields + - cockpit `Save Digest Defaults / Use Saved Digest Defaults / Save Workflow Defaults / Use Saved Workflow Defaults` +- 下一刀最小闭环切片改为: + - `E12 external context visual evidence extension / screenshot readiness starter slice` + +## 10. 2026-05-15 E12 Addendum + +- `E12 external context visual evidence extension / screenshot readiness starter slice` 已完成。 +- 当前已具备: + - screenshot readiness summary + - capture surface screenshot status / browser capture status starter fields + - cockpit `Open Screenshot Surface` +- 下一刀最小闭环切片改为: + - `W9 workflow archive deep bridge extension / bundle export persistence slice` + - A-E 璁″垝宸茬粡鍏ㄩ儴瀹屾垚锛屼笉璇ュ啀浣滀负涓?backlog 閲嶅鎺ㄨ繘銆?- 褰撳墠鐪熸鏈畬鎴愮殑锛屽凡缁忎粠鈥滃熀纭€浜у搧鍖栤€濆垏鎹㈠埌鈥渁rchive 娣卞寲銆乪xternal evidence 寮哄寲銆佽繍钀ラ暱鏈熻鍥锯€濊繖涓€绾с€?- 鐪熷疄浜ゆ槗銆乴ive adapter銆佷笅鍗曟潈闄愮浉鍏冲唴瀹圭户缁繚鎸佸欢鏈燂紝涓嶅簲娣峰洖褰撳墠鍒嗘瀽鍔╂墜涓荤嚎銆? + diff --git a/src/api/alerts.py b/src/api/alerts.py index 59a1cb5..5a1a691 100644 --- a/src/api/alerts.py +++ b/src/api/alerts.py @@ -453,6 +453,11 @@ def get_operator_digest_history_archive( items, persisted_snapshot_id=baseline_state.get("digest_snapshot_id"), ) + profile_scope_summary = _build_archive_profile_scope_summary( + items=items, + profile_scope=normalized_scope, + selected_profile_key=profile_key, + ) return { "status": "active" if items else "idle", "instrument_id": instrument_id, @@ -462,6 +467,7 @@ def get_operator_digest_history_archive( "query_preset_summary": effective.get("query_preset_summary"), "available_query_presets": _digest_archive_available_query_presets(), "available_profile_scopes": ["selected_profile", "all_profiles"], + "profile_scope_summary": profile_scope_summary, "items": items, "item_count": len(items), "offset": offset, @@ -580,6 +586,7 @@ def export_operator_digest_history_archive( "profile_key": profile_key, "profile_scope": archive.get("profile_scope"), "query_preset": archive.get("query_preset"), + "profile_scope_summary": dict(archive.get("profile_scope_summary") or {}), "filters": dict(archive.get("filters") or {}), "item_count": archive.get("item_count") or 0, "items": list(archive.get("items") or []), @@ -639,6 +646,11 @@ def get_operator_workflow_archive( items, persisted_snapshot_id=baseline_state.get("workflow_snapshot_id"), ) + profile_scope_summary = _build_archive_profile_scope_summary( + items=items, + profile_scope=normalized_scope, + selected_profile_key=profile_key, + ) return { "status": "active" if items else "idle", "instrument_id": instrument_id, @@ -648,6 +660,7 @@ def get_operator_workflow_archive( "query_preset_summary": effective.get("query_preset_summary"), "available_query_presets": _workflow_archive_available_query_presets(), "available_profile_scopes": ["selected_profile", "all_profiles"], + "profile_scope_summary": profile_scope_summary, "items": items, "item_count": len(items), "offset": offset, @@ -771,6 +784,7 @@ def export_operator_workflow_archive( "profile_key": profile_key, "profile_scope": archive.get("profile_scope"), "query_preset": archive.get("query_preset"), + "profile_scope_summary": dict(archive.get("profile_scope_summary") or {}), "filters": dict(archive.get("filters") or {}), "item_count": archive.get("item_count") or 0, "items": list(archive.get("items") or []), @@ -1104,6 +1118,24 @@ def _fetch_workflow_history_rows_by_ids( return filtered +def _build_archive_profile_scope_summary(*, items: list[dict], profile_scope: str, selected_profile_key: str) -> dict: + profile_counts: dict[str, int] = {} + for item in items: + profile_key = str(item.get("profile_key") or "unknown") + profile_counts[profile_key] = int(profile_counts.get(profile_key) or 0) + 1 + most_active_profile = None + if profile_counts: + most_active_profile = sorted(profile_counts.items(), key=lambda row: (-row[1], row[0]))[0][0] + return { + "status": "active" if items else "idle", + "scope": profile_scope, + "selected_profile_key": selected_profile_key, + "profile_count": len(profile_counts), + "profile_counts": profile_counts, + "most_active_profile": most_active_profile, + } + + def _normalize_digest_history_row(row: dict) -> dict: payload = dict(row.get("payload") or {}) return { @@ -1371,6 +1403,8 @@ def _build_digest_archive_structured_export_summary(archive: dict, compare: dict kind_counts[history_kind] = int(kind_counts.get(history_kind) or 0) + 1 return { "status": archive.get("status") or "idle", + "profile_scope": archive.get("profile_scope"), + "query_preset": archive.get("query_preset"), "profile_count": len(profile_counts), "profile_counts": profile_counts, "history_kind_counts": kind_counts, @@ -1394,6 +1428,8 @@ def _build_workflow_archive_structured_export_summary(archive: dict, compare: di phase_counts[phase_key] = int(phase_counts.get(phase_key) or 0) + 1 return { "status": archive.get("status") or "idle", + "profile_scope": archive.get("profile_scope"), + "query_preset": archive.get("query_preset"), "profile_count": len(profile_counts), "profile_counts": profile_counts, "session_counts": session_counts, diff --git a/src/services/operator/operator_workspace_service.py b/src/services/operator/operator_workspace_service.py index 0e6d8b8..acc3dc0 100644 --- a/src/services/operator/operator_workspace_service.py +++ b/src/services/operator/operator_workspace_service.py @@ -6,6 +6,7 @@ import hashlib import json from typing import Optional +from src.config.settings import settings from src.repositories.decision_packet_repository import DecisionPacketRepository from src.repositories.decision_assistant_repository import DecisionAssistantRepository from src.repositories.market_session_repository import MarketSessionRepository @@ -682,6 +683,25 @@ def _decision_assistant_archive_baselines(persisted_digest_preference: dict) -> } +def _decision_assistant_archive_defaults(persisted_digest_preference: dict) -> dict: + payload = _decision_assistant_preference_payload(persisted_digest_preference) + defaults = dict(payload.get("archive_defaults") or {}) + digest_defaults = dict(defaults.get("digest") or {}) + workflow_defaults = dict(defaults.get("workflow") or {}) + status = "persisted" if digest_defaults or workflow_defaults else "not_saved" + return { + "status": status, + "digest": { + "profile_scope": digest_defaults.get("profile_scope") or "selected_profile", + "query_preset": digest_defaults.get("query_preset") or "", + }, + "workflow": { + "profile_scope": workflow_defaults.get("profile_scope") or "selected_profile", + "query_preset": workflow_defaults.get("query_preset") or "", + }, + } + + def _decision_assistant_importance_template_thresholds(*, persisted_digest_preference: dict) -> dict: payload = _decision_assistant_preference_payload(persisted_digest_preference) thresholds = dict(payload.get("importance_templates") or {}) @@ -2273,6 +2293,7 @@ def _build_decision_assistant_external_context_summary( provider_health_cards=provider_health_cards, ) capture_surface_summary = _build_external_context_capture_surface_summary(capture_surface_cards) + screenshot_readiness_summary = _build_external_context_screenshot_readiness_summary(capture_surface_cards) chart_adapter_mode = _external_context_chart_adapter_mode(chart_readonly_sources) return { "status": "active" if active_events or external_waiting_items or council_watch_items or operator_questions or next_confirmation_targets or exchange_events or announcements else "idle", @@ -2325,6 +2346,7 @@ def _build_decision_assistant_external_context_summary( "capture_surface_card_count": len(capture_surface_cards), "capture_surface_status_counts": _external_context_inventory_status_counts(capture_surface_cards), "capture_surface_summary": capture_surface_summary, + "screenshot_readiness_summary": screenshot_readiness_summary, "source_cards": [ { "source_type": "news_provider", @@ -4028,6 +4050,20 @@ def _external_context_capture_readiness(*, target_url: str | None, embed_url: st return "no_surface" +def _external_context_screenshot_capture_status(*, capture_supported: bool) -> str: + if not capture_supported: + return "capture_not_supported" + if not bool(settings.visual_browser_capture_enabled): + return "browser_capture_disabled" + if str(settings.visual_browser_capture_browser_path or "").strip(): + return "configured_browser_ready" + return "browser_autodiscovery_needed" + + +def _external_context_screenshot_embed_status(*, embed_url: str | None) -> str: + return "embed_ready" if str(embed_url or "").strip() else "embed_not_available" + + def _build_external_context_capture_surface_cards( *, visual_reference_cards: list[dict], @@ -4060,6 +4096,17 @@ def _build_external_context_capture_surface_cards( "target_url": target_url, "embed_url": embed_url, "evidence_bridge_status": "active" if target_url or embed_url else "idle", + "screenshot_capture_status": _external_context_screenshot_capture_status( + capture_supported=capture_supported, + ), + "screenshot_embed_status": _external_context_screenshot_embed_status(embed_url=embed_url), + "browser_capture_enabled": bool(settings.visual_browser_capture_enabled), + "browser_path_configured": bool(str(settings.visual_browser_capture_browser_path or "").strip()), + "screenshot_timeout_seconds": int(settings.visual_browser_capture_timeout_seconds or 0), + "screenshot_viewport": { + "width": int(settings.visual_browser_capture_viewport_width or 0), + "height": int(settings.visual_browser_capture_viewport_height or 0), + }, "capture_surface_prompt": item.get("visual_prompt") or item.get("next_visual_prompt"), } ) @@ -4100,6 +4147,60 @@ def _build_external_context_capture_surface_summary(cards: list[dict]) -> dict: } +def _build_external_context_screenshot_readiness_summary(cards: list[dict]) -> dict: + if not cards: + return { + "status": "idle", + "browser_capture_enabled": bool(settings.visual_browser_capture_enabled), + "browser_path_configured": bool(str(settings.visual_browser_capture_browser_path or "").strip()), + "screenshot_ready_count": 0, + "autodiscovery_needed_count": 0, + "embed_ready_count": 0, + "next_screenshot_target": None, + "next_screenshot_prompt": None, + } + screenshot_ready_count = sum( + 1 + for item in cards + if str(item.get("screenshot_capture_status") or "") == "configured_browser_ready" + ) + autodiscovery_needed_count = sum( + 1 + for item in cards + if str(item.get("screenshot_capture_status") or "") == "browser_autodiscovery_needed" + ) + embed_ready_count = sum( + 1 + for item in cards + if str(item.get("screenshot_embed_status") or "") == "embed_ready" + ) + next_target = next( + ( + dict(item) + for item in cards + if str(item.get("screenshot_capture_status") or "") in {"configured_browser_ready", "browser_autodiscovery_needed"} + ), + dict(cards[0] or {}), + ) + return { + "status": "active", + "browser_capture_enabled": bool(settings.visual_browser_capture_enabled), + "browser_path_configured": bool(str(settings.visual_browser_capture_browser_path or "").strip()), + "screenshot_ready_count": screenshot_ready_count, + "autodiscovery_needed_count": autodiscovery_needed_count, + "embed_ready_count": embed_ready_count, + "viewport": { + "width": int(settings.visual_browser_capture_viewport_width or 0), + "height": int(settings.visual_browser_capture_viewport_height or 0), + }, + "timeout_seconds": int(settings.visual_browser_capture_timeout_seconds or 0), + "next_screenshot_target": next_target.get("title") or next_target.get("provider_name"), + "next_screenshot_status": next_target.get("screenshot_capture_status"), + "next_screenshot_prompt": next_target.get("capture_surface_prompt"), + "next_screenshot_url": next_target.get("target_url") or next_target.get("embed_url"), + } + + def _build_external_context_visual_reference_cards( *, macro_news_provider_inventory: list[dict], @@ -4688,6 +4789,7 @@ def _build_digest_preference_profile( ) -> dict: preference_payload = _decision_assistant_preference_payload(persisted_digest_preference) archive_baselines = _decision_assistant_archive_baselines(persisted_digest_preference) + archive_defaults = _decision_assistant_archive_defaults(persisted_digest_preference) threshold_overrides = dict(preference_payload.get("importance_templates") or {}) template_counts = _digest_importance_template_counts(personalized_reminders) preferred_template = next( @@ -4744,6 +4846,7 @@ def _build_digest_preference_profile( "available_profiles": available_profiles, "focus_candidate_id": dict(focus_candidate or {}).get("id"), "archive_baselines": archive_baselines, + "archive_defaults": archive_defaults, "high_value_only_recommended": high_value_count > 0, "profile_reason": _digest_preference_profile_reason( preferred_template=preferred_template, @@ -9133,17 +9236,19 @@ def _build_decision_assistant_evidence_chain( }, ) capture_surface_summary = dict((external_context_summary or {}).get("capture_surface_summary") or {}) + screenshot_readiness_summary = dict((external_context_summary or {}).get("screenshot_readiness_summary") or {}) if capture_surface_summary.get("status") == "active": chain.append( { "title": "External Visual Evidence", - "status": capture_surface_summary.get("next_capture_readiness") or "active", + "status": screenshot_readiness_summary.get("next_screenshot_status") or capture_surface_summary.get("next_capture_readiness") or "active", "detail": ( f"{capture_surface_summary.get('next_capture_target') or 'external surface'} | " f"capture_ready={capture_surface_summary.get('capture_ready_count') or 0} | " - f"embed_ready={capture_surface_summary.get('embed_ready_count') or 0}" + f"embed_ready={capture_surface_summary.get('embed_ready_count') or 0} | " + f"screenshot_ready={screenshot_readiness_summary.get('screenshot_ready_count') or 0}" ), - "reason": capture_surface_summary.get("next_capture_prompt") or "external_visual_capture_surface", + "reason": screenshot_readiness_summary.get("next_screenshot_prompt") or capture_surface_summary.get("next_capture_prompt") or "external_visual_capture_surface", } ) return chain diff --git a/src/web/app.js b/src/web/app.js index 3889652..8d70590 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -427,6 +427,10 @@ function buildAssistantPreferenceDraft(summary) { history_window_hours: Number(profile.history_window_hours || 24), importance_templates: templates, focus_candidate_id: profile.focus_candidate_id ?? summary?.focus_workspace_context?.focus_candidate_id ?? null, + archive_defaults: profile.archive_defaults || { + digest: { profile_scope: 'selected_profile', query_preset: '' }, + workflow: { profile_scope: 'selected_profile', query_preset: '' }, + }, }; } @@ -466,6 +470,19 @@ function updateAssistantPreferenceTemplateDraft(template, threshold) { updateAssistantPreferenceDraft({ importance_templates: nextTemplates }); } +function mergeAssistantArchiveDefaults(summary, patch = {}) { + const profile = summary?.digest_surface?.digest_preference_profile || {}; + const currentDefaults = { + digest: { profile_scope: 'selected_profile', query_preset: '' }, + workflow: { profile_scope: 'selected_profile', query_preset: '' }, + ...((assistantPreferenceDraft && assistantPreferenceDraft.archive_defaults) || profile.archive_defaults || {}), + }; + return { + ...currentDefaults, + ...(patch || {}), + }; +} + function resetAssistantPreferenceDraftToServer(summary) { assistantPreferenceDraft = buildAssistantPreferenceDraft(summary); assistantPreferenceDraftDirty = false; @@ -972,6 +989,7 @@ function renderDecisionAssistant(summary) { : (digestSurface?.digest_archive_compare || summary.digest_archive_compare || null); const digestArchiveLongRangeSummary = digestArchiveData?.long_range_trend_summary || digestSurface?.digest_history_archive?.long_range_trend_summary || null; const digestArchiveBaselineSummary = digestArchiveData?.baseline_pin_summary || digestSurface?.digest_history_archive?.baseline_pin_summary || null; + const digestArchiveSavedDefaults = digestPreferenceProfile?.archive_defaults?.digest || { profile_scope: 'selected_profile', query_preset: '' }; const workflowArchiveData = window.__assistantWorkflowArchiveResponse?.instrument_id === Number(instrumentIdInput.value || 1) ? window.__assistantWorkflowArchiveResponse : summary.workflow_artifact_summary?.workflow_history_archive; @@ -980,6 +998,7 @@ function renderDecisionAssistant(summary) { : (summary.workflow_artifact_summary?.workflow_archive_compare || summary.workflow_archive_compare || null); const workflowArchiveLongRangeSummary = workflowArchiveData?.long_range_trend_summary || summary.workflow_artifact_summary?.workflow_history_archive?.long_range_trend_summary || null; const workflowArchiveBaselineSummary = workflowArchiveData?.baseline_pin_summary || summary.workflow_artifact_summary?.workflow_history_archive?.baseline_pin_summary || null; + const workflowArchiveSavedDefaults = digestPreferenceProfile?.archive_defaults?.workflow || { profile_scope: 'selected_profile', query_preset: '' }; const workflowArchiveOperationsSummary = workflowArchiveData?.operations_summary || summary.workflow_artifact_summary?.workflow_archive_operations_summary || summary.workflow_archive_operations_summary @@ -1627,6 +1646,7 @@ function renderDecisionAssistant(summary) { profile_label: assistantPreferenceDraft?.profile_label || selectedProfileKey, history_window_hours: assistantPreferenceDraft?.history_window_hours || 24, importance_templates: { ...((assistantPreferenceDraft && assistantPreferenceDraft.importance_templates) || {}) }, + archive_defaults: { ...((assistantPreferenceDraft && assistantPreferenceDraft.archive_defaults) || {}) }, }, }); if (!saved) { @@ -1797,7 +1817,7 @@ function renderDecisionAssistant(summary) { ['selected_profile', 'all_profiles'].forEach((scope) => { const option = createElement('option', '', scope); option.value = scope; - option.selected = scope === (assistantDigestArchiveState.profile_scope || 'selected_profile'); + option.selected = scope === (assistantDigestArchiveState.profile_scope || digestArchiveSavedDefaults.profile_scope || 'selected_profile'); digestArchiveScopeSelect.appendChild(option); }); digestArchiveScopeField.appendChild(digestArchiveScopeSelect); @@ -1809,7 +1829,7 @@ function renderDecisionAssistant(summary) { [{ preset_id: '', label: 'custom' }, ...((digestArchiveData?.available_query_presets || []))].forEach((preset) => { const option = createElement('option', '', preset.label || preset.preset_id || 'custom'); option.value = preset.preset_id || ''; - option.selected = (preset.preset_id || '') === (assistantDigestArchiveState.query_preset || ''); + option.selected = (preset.preset_id || '') === (assistantDigestArchiveState.query_preset || digestArchiveSavedDefaults.query_preset || ''); digestArchivePresetSelect.appendChild(option); }); digestArchivePresetField.appendChild(digestArchivePresetSelect); @@ -1997,6 +2017,52 @@ function renderDecisionAssistant(summary) { await refreshDashboard(); }, }, + { + label: 'Save Digest Defaults', + onClick: async () => { + const saved = await persistAssistantDigestPreferenceServer({ + profileKey: selectedProfileKey, + digestMode: assistantPreferenceDraft?.digest_mode || digestPreferenceProfile?.persisted_digest_mode || 'all_reminders', + preferredHistoryKind: assistantPreferenceDraft?.preferred_history_kind === 'all' + ? null + : (assistantPreferenceDraft?.preferred_history_kind || digestPreferenceProfile?.preferred_history_kind || null), + importanceThreshold: assistantPreferenceDraft?.importance_threshold || digestPreferenceProfile?.importance_threshold || 70, + payload: { + profile_label: assistantPreferenceDraft?.profile_label || digestPreferenceProfile?.profile_label || selectedProfileKey, + history_window_hours: assistantPreferenceDraft?.history_window_hours || digestPreferenceProfile?.history_window_hours || 24, + importance_templates: { ...((assistantPreferenceDraft && assistantPreferenceDraft.importance_templates) || {}) }, + archive_defaults: mergeAssistantArchiveDefaults(summary, { + digest: { + profile_scope: digestArchiveScopeSelect.value || 'selected_profile', + query_preset: digestArchivePresetSelect.value || '', + }, + }), + }, + }); + if (!saved) { + showErrorPanel('digest_archive_defaults_save_failed'); + return; + } + assistantPreferenceDraft = { + ...(assistantPreferenceDraft || {}), + archive_defaults: mergeAssistantArchiveDefaults(summary, { + digest: { + profile_scope: digestArchiveScopeSelect.value || 'selected_profile', + query_preset: digestArchivePresetSelect.value || '', + }, + }), + }; + await refreshDashboard(); + }, + }, + { + label: 'Use Saved Digest Defaults', + onClick: () => { + assistantDigestArchiveState.profile_scope = digestArchiveSavedDefaults.profile_scope || 'selected_profile'; + assistantDigestArchiveState.query_preset = digestArchiveSavedDefaults.query_preset || ''; + renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary); + }, + }, { label: 'Clear Archive Filters', onClick: () => { @@ -2025,9 +2091,10 @@ function renderDecisionAssistant(summary) { digestArchivePanel.appendChild( buildInfoGrid([ ['archive query profile', assistantDigestArchiveState.profile_key || selectedProfileKey], - ['archive profile scope', assistantDigestArchiveState.profile_scope || 'selected_profile'], - ['archive preset', assistantDigestArchiveState.query_preset || 'custom'], + ['archive profile scope', assistantDigestArchiveState.profile_scope || digestArchiveSavedDefaults.profile_scope || 'selected_profile'], + ['archive preset', assistantDigestArchiveState.query_preset || digestArchiveSavedDefaults.query_preset || 'custom'], ['archive preset summary', digestArchiveData?.query_preset_summary || 'custom_filters'], + ['saved archive defaults', `${digestArchiveSavedDefaults.profile_scope || 'selected_profile'} / ${digestArchiveSavedDefaults.query_preset || 'custom'}`], ['archive query mode', assistantDigestArchiveState.digest_mode || 'all'], ['archive query kind', assistantDigestArchiveState.preferred_history_kind || 'all'], ['archive query text', assistantDigestArchiveState.query_text || 'n/a'], @@ -2611,7 +2678,7 @@ function renderDecisionAssistant(summary) { ['selected_profile', 'all_profiles'].forEach((scope) => { const option = createElement('option', '', scope); option.value = scope; - option.selected = scope === (assistantWorkflowArchiveState.profile_scope || 'selected_profile'); + option.selected = scope === (assistantWorkflowArchiveState.profile_scope || workflowArchiveSavedDefaults.profile_scope || 'selected_profile'); workflowScopeSelect.appendChild(option); }); workflowScopeField.appendChild(workflowScopeSelect); @@ -2623,7 +2690,7 @@ function renderDecisionAssistant(summary) { [{ preset_id: '', label: 'custom' }, ...((workflowArchiveData?.available_query_presets || []))].forEach((preset) => { const option = createElement('option', '', preset.label || preset.preset_id || 'custom'); option.value = preset.preset_id || ''; - option.selected = (preset.preset_id || '') === (assistantWorkflowArchiveState.query_preset || ''); + option.selected = (preset.preset_id || '') === (assistantWorkflowArchiveState.query_preset || workflowArchiveSavedDefaults.query_preset || ''); workflowPresetSelect.appendChild(option); }); workflowPresetField.appendChild(workflowPresetSelect); @@ -2820,6 +2887,52 @@ function renderDecisionAssistant(summary) { await refreshDashboard(); }, }, + { + label: 'Save Workflow Defaults', + onClick: async () => { + const saved = await persistAssistantDigestPreferenceServer({ + profileKey: selectedProfileKey, + digestMode: assistantPreferenceDraft?.digest_mode || digestPreferenceProfile?.persisted_digest_mode || 'all_reminders', + preferredHistoryKind: assistantPreferenceDraft?.preferred_history_kind === 'all' + ? null + : (assistantPreferenceDraft?.preferred_history_kind || digestPreferenceProfile?.preferred_history_kind || null), + importanceThreshold: assistantPreferenceDraft?.importance_threshold || digestPreferenceProfile?.importance_threshold || 70, + payload: { + profile_label: assistantPreferenceDraft?.profile_label || digestPreferenceProfile?.profile_label || selectedProfileKey, + history_window_hours: assistantPreferenceDraft?.history_window_hours || digestPreferenceProfile?.history_window_hours || 24, + importance_templates: { ...((assistantPreferenceDraft && assistantPreferenceDraft.importance_templates) || {}) }, + archive_defaults: mergeAssistantArchiveDefaults(summary, { + workflow: { + profile_scope: workflowScopeSelect.value || 'selected_profile', + query_preset: workflowPresetSelect.value || '', + }, + }), + }, + }); + if (!saved) { + showErrorPanel('workflow_archive_defaults_save_failed'); + return; + } + assistantPreferenceDraft = { + ...(assistantPreferenceDraft || {}), + archive_defaults: mergeAssistantArchiveDefaults(summary, { + workflow: { + profile_scope: workflowScopeSelect.value || 'selected_profile', + query_preset: workflowPresetSelect.value || '', + }, + }), + }; + await refreshDashboard(); + }, + }, + { + label: 'Use Saved Workflow Defaults', + onClick: () => { + assistantWorkflowArchiveState.profile_scope = workflowArchiveSavedDefaults.profile_scope || 'selected_profile'; + assistantWorkflowArchiveState.query_preset = workflowArchiveSavedDefaults.query_preset || ''; + renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary); + }, + }, { label: 'Clear Workflow Filters', onClick: () => { @@ -2849,9 +2962,10 @@ function renderDecisionAssistant(summary) { workflowArchivePanel.appendChild( buildInfoGrid([ ['workflow query profile', assistantWorkflowArchiveState.profile_key || selectedProfileKey], - ['workflow profile scope', assistantWorkflowArchiveState.profile_scope || 'selected_profile'], - ['workflow preset', assistantWorkflowArchiveState.query_preset || 'custom'], + ['workflow profile scope', assistantWorkflowArchiveState.profile_scope || workflowArchiveSavedDefaults.profile_scope || 'selected_profile'], + ['workflow preset', assistantWorkflowArchiveState.query_preset || workflowArchiveSavedDefaults.query_preset || 'custom'], ['workflow preset summary', workflowArchiveData?.query_preset_summary || 'custom_filters'], + ['saved workflow defaults', `${workflowArchiveSavedDefaults.profile_scope || 'selected_profile'} / ${workflowArchiveSavedDefaults.query_preset || 'custom'}`], ['workflow query session', assistantWorkflowArchiveState.current_session || 'all'], ['workflow query phase', assistantWorkflowArchiveState.next_phase || 'all'], ['workflow query artifact', assistantWorkflowArchiveState.next_artifact_kind || 'all'], @@ -3387,6 +3501,12 @@ function renderDecisionAssistant(summary) { ['capture surface states', formatJsonInline(externalContext.capture_surface_status_counts || {})], ['capture next target', externalContext.capture_surface_summary?.next_capture_target || 'n/a'], ['capture next prompt', externalContext.capture_surface_summary?.next_capture_prompt || 'n/a'], + ['screenshot readiness', externalContext.screenshot_readiness_summary?.status || 'n/a'], + ['screenshot ready', String(externalContext.screenshot_readiness_summary?.screenshot_ready_count ?? 0)], + ['autodiscovery needed', String(externalContext.screenshot_readiness_summary?.autodiscovery_needed_count ?? 0)], + ['screenshot embed ready', String(externalContext.screenshot_readiness_summary?.embed_ready_count ?? 0)], + ['screenshot next target', externalContext.screenshot_readiness_summary?.next_screenshot_target || 'n/a'], + ['screenshot next prompt', externalContext.screenshot_readiness_summary?.next_screenshot_prompt || 'n/a'], ['visual references', String(externalContext.visual_reference_card_count ?? 0)], ['visual prompt', externalContext.visual_reference_next_prompt || 'n/a'], ['visual evidence bridge', externalContext.visual_evidence_bridge_summary?.status || 'n/a'], @@ -3423,6 +3543,16 @@ function renderDecisionAssistant(summary) { ]) ); } + if (externalContext.screenshot_readiness_summary?.next_screenshot_url) { + decisionAssistantPanel.appendChild( + buildActionBar([ + { + label: 'Open Screenshot Surface', + onClick: () => openExternalContextLink(externalContext.screenshot_readiness_summary?.next_screenshot_url), + }, + ]) + ); + } const sourceCards = externalContext.source_cards || []; if (sourceCards.length) { const sourceGrid = createElement('div', 'cards'); @@ -3518,8 +3648,14 @@ function renderDecisionAssistant(summary) { [ ['group', item.surface_group || 'n/a'], ['capture', String(Boolean(item.capture_supported))], + ['screenshot status', item.screenshot_capture_status || 'n/a'], + ['embed status', item.screenshot_embed_status || 'n/a'], ['embed', item.embed_ready ? 'ready' : 'n/a'], ['bridge', item.evidence_bridge_status || 'n/a'], + ['browser capture', String(Boolean(item.browser_capture_enabled))], + ['browser configured', String(Boolean(item.browser_path_configured))], + ['screenshot timeout', String(item.screenshot_timeout_seconds ?? 'n/a')], + ['viewport', formatJsonInline(item.screenshot_viewport || {})], ['target', item.target_url || 'n/a'], ['embed url', item.embed_url || 'n/a'], ['prompt', item.capture_surface_prompt || 'n/a'], diff --git a/tests/unit/test_api_static_server.py b/tests/unit/test_api_static_server.py index 210874b..9d6dd97 100644 --- a/tests/unit/test_api_static_server.py +++ b/tests/unit/test_api_static_server.py @@ -157,10 +157,14 @@ class ApiStaticServerTests(unittest.TestCase): "provider health cards", "Open Visual Bridge", "Open Capture Surface", + "Open Screenshot Surface", "Open Target", "Open Embed", "Open Capture Target", "Open Capture Embed", + "screenshot readiness", + "screenshot status", + "browser capture", "archive trend", "archive trend story", "archive baseline", @@ -169,6 +173,10 @@ class ApiStaticServerTests(unittest.TestCase): "Compare To Baseline", "Save Baseline", "Save Workflow Baseline", + "Save Digest Defaults", + "Use Saved Digest Defaults", + "Save Workflow Defaults", + "Use Saved Workflow Defaults", "workflow long-range", "workflow baseline", "workflow trend story", diff --git a/tests/unit/test_operator_workspace_service.py b/tests/unit/test_operator_workspace_service.py index d504881..a263bfd 100644 --- a/tests/unit/test_operator_workspace_service.py +++ b/tests/unit/test_operator_workspace_service.py @@ -657,6 +657,8 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertGreaterEqual(summary["external_context_summary"]["capture_surface_card_count"], 3) self.assertIn("capture_surface_summary", summary["external_context_summary"]) self.assertEqual(summary["external_context_summary"]["capture_surface_summary"]["status"], "active") + self.assertIn("screenshot_readiness_summary", summary["external_context_summary"]) + self.assertEqual(summary["external_context_summary"]["screenshot_readiness_summary"]["status"], "active") self.assertIn("visual_reference_cards", summary["external_context_summary"]) self.assertGreaterEqual(summary["external_context_summary"]["visual_reference_card_count"], 3) self.assertIsNotNone(summary["external_context_summary"]["visual_reference_next_prompt"])