@nac3/forge-cli 0.2.1-alpha.60 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/agents/bridge.d.ts.map +1 -1
- package/dist/agents/bridge.js +27 -0
- package/dist/agents/bridge.js.map +1 -1
- package/dist/agents/config_resolver.d.ts.map +1 -1
- package/dist/agents/config_resolver.js +15 -15
- package/dist/agents/config_resolver.js.map +1 -1
- package/dist/agents/runner.d.ts +8 -0
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +58 -11
- package/dist/agents/runner.js.map +1 -1
- package/dist/agents/system_prompt.d.ts.map +1 -1
- package/dist/agents/system_prompt.js +2 -1
- package/dist/agents/system_prompt.js.map +1 -1
- package/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +18 -0
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/claude.d.ts +13 -0
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js +45 -10
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/claude_cli.d.ts +10 -0
- package/dist/chat/claude_cli.d.ts.map +1 -1
- package/dist/chat/claude_cli.js +97 -16
- package/dist/chat/claude_cli.js.map +1 -1
- package/dist/chat/completion_hallucination_detector.d.ts +11 -0
- package/dist/chat/completion_hallucination_detector.d.ts.map +1 -1
- package/dist/chat/completion_hallucination_detector.js +37 -2
- package/dist/chat/completion_hallucination_detector.js.map +1 -1
- package/dist/chat/doctrine_digest.d.ts.map +1 -1
- package/dist/chat/doctrine_digest.js +10 -6
- package/dist/chat/doctrine_digest.js.map +1 -1
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +885 -42
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/refusal_detector.d.ts.map +1 -1
- package/dist/chat/refusal_detector.js +2 -1
- package/dist/chat/refusal_detector.js.map +1 -1
- package/dist/chat/server.d.ts +1 -0
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +890 -62
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/session_projects.d.ts +31 -0
- package/dist/chat/session_projects.d.ts.map +1 -0
- package/dist/chat/session_projects.js +36 -0
- package/dist/chat/session_projects.js.map +1 -0
- package/dist/chat/session_store.d.ts +32 -0
- package/dist/chat/session_store.d.ts.map +1 -0
- package/dist/chat/session_store.js +58 -0
- package/dist/chat/session_store.js.map +1 -0
- package/dist/chat/tooling_section.d.ts +11 -0
- package/dist/chat/tooling_section.d.ts.map +1 -0
- package/dist/chat/tooling_section.js +70 -0
- package/dist/chat/tooling_section.js.map +1 -0
- package/dist/chat/tools/app_backend_dispatch.d.ts.map +1 -1
- package/dist/chat/tools/app_backend_dispatch.js +44 -1
- package/dist/chat/tools/app_backend_dispatch.js.map +1 -1
- package/dist/chat/tools/fs_editor.d.ts.map +1 -1
- package/dist/chat/tools/fs_editor.js +53 -3
- package/dist/chat/tools/fs_editor.js.map +1 -1
- package/dist/chat/tools/graph.js +2 -2
- package/dist/chat/tools/graph.js.map +1 -1
- package/dist/chat/tools/keys.d.ts.map +1 -1
- package/dist/chat/tools/keys.js +5 -18
- package/dist/chat/tools/keys.js.map +1 -1
- package/dist/chat/tools/nac3_invoke.d.ts.map +1 -1
- package/dist/chat/tools/nac3_invoke.js +16 -7
- package/dist/chat/tools/nac3_invoke.js.map +1 -1
- package/dist/chat/tools/shell.d.ts +6 -0
- package/dist/chat/tools/shell.d.ts.map +1 -1
- package/dist/chat/tools/shell.js +39 -0
- package/dist/chat/tools/shell.js.map +1 -1
- package/dist/chat/tools/workflow.d.ts +20 -0
- package/dist/chat/tools/workflow.d.ts.map +1 -1
- package/dist/chat/tools/workflow.js +98 -0
- package/dist/chat/tools/workflow.js.map +1 -1
- package/dist/chat/tools.d.ts.map +1 -1
- package/dist/chat/tools.js +79 -1
- package/dist/chat/tools.js.map +1 -1
- package/dist/chat/turn_control.d.ts +42 -0
- package/dist/chat/turn_control.d.ts.map +1 -0
- package/dist/chat/turn_control.js +53 -0
- package/dist/chat/turn_control.js.map +1 -0
- package/dist/commands/brain.js +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +61 -10
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/graph.js +9 -9
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/keys_setup.d.ts +10 -28
- package/dist/commands/keys_setup.d.ts.map +1 -1
- package/dist/commands/keys_setup.js +54 -228
- package/dist/commands/keys_setup.js.map +1 -1
- package/dist/commands/license.d.ts.map +1 -1
- package/dist/commands/license.js +52 -28
- package/dist/commands/license.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +62 -6
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/mobile.d.ts +1 -1
- package/dist/commands/mobile.d.ts.map +1 -1
- package/dist/commands/mobile.js +32 -28
- package/dist/commands/mobile.js.map +1 -1
- package/dist/commands/onboard.d.ts +19 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +126 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/pending.d.ts.map +1 -1
- package/dist/commands/pending.js +25 -1
- package/dist/commands/pending.js.map +1 -1
- package/dist/commands/projects.js +25 -1
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/review-screens.js +28 -1
- package/dist/commands/review-screens.js.map +1 -1
- package/dist/commands/workflow.d.ts +18 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +253 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/core/brain_config.d.ts +2 -0
- package/dist/core/brain_config.d.ts.map +1 -1
- package/dist/core/brain_config.js +18 -3
- package/dist/core/brain_config.js.map +1 -1
- package/dist/core/cf_tunnel.d.ts.map +1 -1
- package/dist/core/cf_tunnel.js +8 -2
- package/dist/core/cf_tunnel.js.map +1 -1
- package/dist/core/cost_router.d.ts.map +1 -1
- package/dist/core/cost_router.js +11 -2
- package/dist/core/cost_router.js.map +1 -1
- package/dist/core/desktop_open.d.ts +16 -0
- package/dist/core/desktop_open.d.ts.map +1 -0
- package/dist/core/desktop_open.js +114 -0
- package/dist/core/desktop_open.js.map +1 -0
- package/dist/core/keys_envelope.d.ts.map +1 -1
- package/dist/core/keys_envelope.js +5 -1
- package/dist/core/keys_envelope.js.map +1 -1
- package/dist/core/limits_config.d.ts +5 -0
- package/dist/core/limits_config.d.ts.map +1 -1
- package/dist/core/limits_config.js +5 -0
- package/dist/core/limits_config.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +3 -0
- package/dist/core/logger.js.map +1 -1
- package/dist/core/pairing.d.ts +6 -0
- package/dist/core/pairing.d.ts.map +1 -1
- package/dist/core/pairing.js +41 -2
- package/dist/core/pairing.js.map +1 -1
- package/dist/core/provider_keys.d.ts.map +1 -1
- package/dist/core/provider_keys.js +25 -12
- package/dist/core/provider_keys.js.map +1 -1
- package/dist/core/provider_models.js +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +13 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/design/yujin_design_resolver.d.ts +16 -0
- package/dist/design/yujin_design_resolver.d.ts.map +1 -1
- package/dist/design/yujin_design_resolver.js +103 -20
- package/dist/design/yujin_design_resolver.js.map +1 -1
- package/dist/docs/doctrine/default-design-system.md +32 -0
- package/dist/docs/doctrine/office-tools.md +20 -0
- package/dist/docs/doctrine/verb-composition.md +16 -0
- package/dist/docs/doctrine/workflow-kinds.md +39 -22
- package/dist/docs/doctrine/workflow.md +12 -4
- package/dist/license/auto_sync.d.ts +22 -0
- package/dist/license/auto_sync.d.ts.map +1 -0
- package/dist/license/auto_sync.js +91 -0
- package/dist/license/auto_sync.js.map +1 -0
- package/dist/license/handle_key.d.ts +32 -0
- package/dist/license/handle_key.d.ts.map +1 -0
- package/dist/license/handle_key.js +56 -0
- package/dist/license/handle_key.js.map +1 -0
- package/dist/license/hito4_client.d.ts.map +1 -1
- package/dist/license/hito4_client.js +63 -25
- package/dist/license/hito4_client.js.map +1 -1
- package/dist/llm/gemini_client.d.ts.map +1 -1
- package/dist/llm/gemini_client.js +5 -2
- package/dist/llm/gemini_client.js.map +1 -1
- package/dist/llm/managed_client.d.ts.map +1 -1
- package/dist/llm/managed_client.js +14 -9
- package/dist/llm/managed_client.js.map +1 -1
- package/dist/mcp/plan_bridges.d.ts +17 -0
- package/dist/mcp/plan_bridges.d.ts.map +1 -0
- package/dist/mcp/plan_bridges.js +119 -0
- package/dist/mcp/plan_bridges.js.map +1 -0
- package/dist/migrate/ai-apply.d.ts.map +1 -1
- package/dist/migrate/ai-apply.js +25 -2
- package/dist/migrate/ai-apply.js.map +1 -1
- package/dist/nac3/internal_manifest.d.ts.map +1 -1
- package/dist/nac3/internal_manifest.js +200 -10
- package/dist/nac3/internal_manifest.js.map +1 -1
- package/dist/nac3/verb_tier_map.d.ts.map +1 -1
- package/dist/nac3/verb_tier_map.js +3 -0
- package/dist/nac3/verb_tier_map.js.map +1 -1
- package/dist/onboarding/concierge_client.d.ts +34 -0
- package/dist/onboarding/concierge_client.d.ts.map +1 -0
- package/dist/onboarding/concierge_client.js +80 -0
- package/dist/onboarding/concierge_client.js.map +1 -0
- package/dist/onboarding/next_step.d.ts +32 -0
- package/dist/onboarding/next_step.d.ts.map +1 -0
- package/dist/onboarding/next_step.js +60 -0
- package/dist/onboarding/next_step.js.map +1 -0
- package/dist/pending/store.d.ts +7 -0
- package/dist/pending/store.d.ts.map +1 -1
- package/dist/pending/store.js +19 -0
- package/dist/pending/store.js.map +1 -1
- package/dist/pending/types.d.ts +20 -0
- package/dist/pending/types.d.ts.map +1 -1
- package/dist/pending/types.js.map +1 -1
- package/dist/reader/parsers/epub.d.ts.map +1 -1
- package/dist/reader/parsers/epub.js +6 -1
- package/dist/reader/parsers/epub.js.map +1 -1
- package/dist/relay/board_snapshot.d.ts +69 -0
- package/dist/relay/board_snapshot.d.ts.map +1 -0
- package/dist/relay/board_snapshot.js +149 -0
- package/dist/relay/board_snapshot.js.map +1 -0
- package/dist/relay/command_consumer.d.ts +91 -0
- package/dist/relay/command_consumer.d.ts.map +1 -0
- package/dist/relay/command_consumer.js +129 -0
- package/dist/relay/command_consumer.js.map +1 -0
- package/dist/relay/mock_relay.d.ts +34 -0
- package/dist/relay/mock_relay.d.ts.map +1 -0
- package/dist/relay/mock_relay.js +93 -0
- package/dist/relay/mock_relay.js.map +1 -0
- package/dist/relay/relay_client.d.ts +28 -0
- package/dist/relay/relay_client.d.ts.map +1 -0
- package/dist/relay/relay_client.js +80 -0
- package/dist/relay/relay_client.js.map +1 -0
- package/dist/relay/relay_publisher.d.ts +59 -0
- package/dist/relay/relay_publisher.d.ts.map +1 -0
- package/dist/relay/relay_publisher.js +101 -0
- package/dist/relay/relay_publisher.js.map +1 -0
- package/dist/relay/types.d.ts +54 -0
- package/dist/relay/types.d.ts.map +1 -0
- package/dist/relay/types.js +8 -0
- package/dist/relay/types.js.map +1 -0
- package/dist/semantic_graph/ast_extract.d.ts +5 -0
- package/dist/semantic_graph/ast_extract.d.ts.map +1 -1
- package/dist/semantic_graph/ast_extract.js +46 -0
- package/dist/semantic_graph/ast_extract.js.map +1 -1
- package/dist/semantic_graph/derive.d.ts +12 -0
- package/dist/semantic_graph/derive.d.ts.map +1 -1
- package/dist/semantic_graph/derive.js +325 -6
- package/dist/semantic_graph/derive.js.map +1 -1
- package/dist/semantic_graph/store.d.ts +9 -0
- package/dist/semantic_graph/store.d.ts.map +1 -1
- package/dist/semantic_graph/store.js +23 -1
- package/dist/semantic_graph/store.js.map +1 -1
- package/dist/semantic_graph/types.d.ts +1 -1
- package/dist/semantic_graph/types.d.ts.map +1 -1
- package/dist/support/github_dispatcher.d.ts.map +1 -1
- package/dist/support/github_dispatcher.js +30 -2
- package/dist/support/github_dispatcher.js.map +1 -1
- package/dist/tasks/supervisor.d.ts +88 -0
- package/dist/tasks/supervisor.d.ts.map +1 -0
- package/dist/tasks/supervisor.js +308 -0
- package/dist/tasks/supervisor.js.map +1 -0
- package/dist/tasks/task_type.d.ts +11 -0
- package/dist/tasks/task_type.d.ts.map +1 -0
- package/dist/tasks/task_type.js +21 -0
- package/dist/tasks/task_type.js.map +1 -0
- package/dist/vault/store.d.ts.map +1 -1
- package/dist/vault/store.js +6 -1
- package/dist/vault/store.js.map +1 -1
- package/dist/version.d.ts +3 -5
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +3 -5
- package/dist/version.js.map +1 -1
- package/dist/voice/providers/google.d.ts.map +1 -1
- package/dist/voice/providers/google.js +6 -4
- package/dist/voice/providers/google.js.map +1 -1
- package/dist/workflow/config.d.ts +43 -0
- package/dist/workflow/config.d.ts.map +1 -0
- package/dist/workflow/config.js +199 -0
- package/dist/workflow/config.js.map +1 -0
- package/dist/workflow/instances.d.ts +40 -0
- package/dist/workflow/instances.d.ts.map +1 -0
- package/dist/workflow/instances.js +122 -0
- package/dist/workflow/instances.js.map +1 -0
- package/dist/workflow/multi_view.d.ts +27 -0
- package/dist/workflow/multi_view.d.ts.map +1 -0
- package/dist/workflow/multi_view.js +76 -0
- package/dist/workflow/multi_view.js.map +1 -0
- package/dist/workflow/orchestrator.d.ts +111 -0
- package/dist/workflow/orchestrator.d.ts.map +1 -0
- package/dist/workflow/orchestrator.js +241 -0
- package/dist/workflow/orchestrator.js.map +1 -0
- package/docs/doctrine/default-design-system.md +32 -0
- package/docs/doctrine/office-tools.md +20 -0
- package/docs/doctrine/verb-composition.md +16 -0
- package/docs/doctrine/workflow-kinds.md +39 -22
- package/docs/doctrine/workflow.md +12 -4
- package/package.json +2 -1
- package/src/i18n/catalogs/ar.json +223 -210
- package/src/i18n/catalogs/de.json +223 -210
- package/src/i18n/catalogs/en.json +223 -210
- package/src/i18n/catalogs/es.json +223 -210
- package/src/i18n/catalogs/fr.json +223 -210
- package/src/i18n/catalogs/hi.json +223 -210
- package/src/i18n/catalogs/it.json +223 -210
- package/src/i18n/catalogs/ja.json +223 -210
- package/src/i18n/catalogs/pt.json +223 -210
- package/src/i18n/catalogs/zh.json +223 -210
package/dist/chat/panel.js
CHANGED
|
@@ -34,6 +34,19 @@ export function renderPanelHtml(cfg) {
|
|
|
34
34
|
const tr = {
|
|
35
35
|
inputPlaceholder: tForLang(lang, 'panel.input.placeholder'),
|
|
36
36
|
sendButton: tForLang(lang, 'panel.button.send'),
|
|
37
|
+
stopButton: tForLang(lang, 'panel.button.stop'),
|
|
38
|
+
statusStopping: tForLang(lang, 'panel.status.stopping'),
|
|
39
|
+
statusNoActiveTurn: tForLang(lang, 'panel.status.noActiveTurn'),
|
|
40
|
+
statusInterruptFailed: tForLang(lang, 'panel.status.interruptFailed'),
|
|
41
|
+
statusTurnInterrupted: tForLang(lang, 'panel.status.turnInterrupted'),
|
|
42
|
+
labelInterrupted: tForLang(lang, 'panel.label.interrupted'),
|
|
43
|
+
queueLabel: tForLang(lang, 'panel.queue.label'),
|
|
44
|
+
queueRemove: tForLang(lang, 'panel.queue.remove'),
|
|
45
|
+
projectSwitchHint: tForLang(lang, 'panel.project.switchHint'),
|
|
46
|
+
projectTitle: tForLang(lang, 'panel.project.title'),
|
|
47
|
+
projectAdd: tForLang(lang, 'panel.project.add'),
|
|
48
|
+
projectAddPrompt: tForLang(lang, 'panel.project.addPrompt'),
|
|
49
|
+
projectSwitching: tForLang(lang, 'panel.project.switching'),
|
|
37
50
|
micButton: tForLang(lang, 'panel.button.mic'),
|
|
38
51
|
micButtonRec: tForLang(lang, 'panel.button.mic.recording'),
|
|
39
52
|
ttsButton: tForLang(lang, 'panel.button.tts'),
|
|
@@ -308,6 +321,26 @@ body {
|
|
|
308
321
|
/* alpha.59i -- shell kill button on active shell.exec tabs */
|
|
309
322
|
#yf-full .pz-tab-kill { margin-left: 4px; width: 18px; height: 18px; border-radius: 50%; border: 1px solid rgba(208,64,64,0.5); background: rgba(208,64,64,0.15); color: #ff8a8a; cursor: pointer; font-size: 10px; line-height: 1; padding: 0; display: inline-flex; align-items: center; justify-content: center; }
|
|
310
323
|
#yf-full .pz-tab-kill:hover { background: rgba(208,64,64,0.35); border-color: rgba(208,64,64,0.8); }
|
|
324
|
+
/* PND-007 -- Tareas tab */
|
|
325
|
+
#yf-full .pz-tasks-wrap { flex: 1; overflow-y: auto; padding: 14px 16px; }
|
|
326
|
+
#yf-full .pz-tasks-new { display: flex; gap: 8px; margin-bottom: 14px; }
|
|
327
|
+
#yf-full .pz-tasks-input { flex: 1; font: inherit; font-size: 13px; padding: 7px 11px; border: 1px solid var(--line, rgba(0,0,0,0.14)); border-radius: 8px; background: var(--bg-1, #fafafa); color: var(--ink, #1a1a1a); }
|
|
328
|
+
#yf-full .pz-tasks-run { font: inherit; font-size: 12px; font-weight: 600; padding: 7px 14px; border: none; border-radius: 8px; background: #4f5b87; color: white; cursor: pointer; }
|
|
329
|
+
#yf-full .pz-tasks-run:hover { background: #3f4a73; }
|
|
330
|
+
#yf-full .pz-tasks-empty { color: var(--ink-subtle, #9a9a9a); font-size: 13px; padding: 20px 4px; text-align: center; }
|
|
331
|
+
#yf-full .pz-task { display: flex; align-items: center; gap: 12px; padding: 11px 12px; border: 1px solid var(--line, rgba(0,0,0,0.10)); border-radius: 10px; margin-bottom: 8px; background: var(--bg-1, #fcfcfc); }
|
|
332
|
+
#yf-full .pz-task-ic { font-size: 20px; width: 26px; text-align: center; flex-shrink: 0; }
|
|
333
|
+
#yf-full .pz-task-main { flex: 1; min-width: 0; }
|
|
334
|
+
#yf-full .pz-task-label { font-size: 13px; font-weight: 600; color: var(--ink, #1a1a1a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
335
|
+
#yf-full .pz-task-cmd { font-size: 11px; color: var(--ink-subtle, #9a9a9a); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
|
|
336
|
+
#yf-full .pz-task-side { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
337
|
+
#yf-full .pz-task-badge { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; border: 1px solid; border-radius: 6px; padding: 2px 7px; }
|
|
338
|
+
#yf-full .pz-task-kill, #yf-full .pz-task-log { font: inherit; font-size: 11px; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--line, rgba(0,0,0,0.16)); background: transparent; color: var(--ink-muted, #6a6a6a); cursor: pointer; }
|
|
339
|
+
#yf-full .pz-task-kill { border-color: rgba(208,64,64,0.5); color: #c8472e; }
|
|
340
|
+
#yf-full .pz-task-del { font: inherit; font-size: 14px; line-height: 1; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--line, rgba(0,0,0,0.16)); background: transparent; color: var(--ink-subtle, #9a9a9a); cursor: pointer; }
|
|
341
|
+
#yf-full .pz-task-del:hover { border-color: rgba(208,64,64,0.5); color: #c8472e; }
|
|
342
|
+
#yf-full .pz-task-logbox:not(:empty) { margin: -2px 0 10px 38px; }
|
|
343
|
+
#yf-full .pz-task-logpre { font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.5; color: var(--ink-muted, #555); background: var(--bg-2, #f4f4f4); border-radius: 8px; padding: 10px 12px; overflow-x: auto; white-space: pre-wrap; max-height: 220px; }
|
|
311
344
|
#yf-full .pz-tab-body {
|
|
312
345
|
flex: 1; overflow: hidden; min-height: 0;
|
|
313
346
|
display: flex; flex-direction: column;
|
|
@@ -569,6 +602,7 @@ body {
|
|
|
569
602
|
#yf-full .pz-cw-row.k-bash { border-left-color: #6f8b5e; }
|
|
570
603
|
#yf-full .pz-cw-row.k-read, #yf-full .pz-cw-row.k-search { border-left-color: var(--indigo, #4f5b87); }
|
|
571
604
|
#yf-full .pz-cw-head { display: flex; align-items: baseline; gap: 7px; }
|
|
605
|
+
#yf-full .pz-cw-ts { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; color: var(--ink-subtle, #a89f88); flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
572
606
|
#yf-full .pz-cw-ic { color: var(--ink-muted, #8a7f68); font-size: 12px; width: 14px; flex: 0 0 auto; text-align: center; }
|
|
573
607
|
#yf-full .pz-cw-target { font-family: ui-monospace, Menlo, monospace; font-size: 13px; color: var(--ink, #1a1a1a); font-weight: 600; word-break: break-all; }
|
|
574
608
|
#yf-full .pz-cw-path { font-size: 10px; color: var(--ink-subtle, #a89f88); margin-left: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 45%; }
|
|
@@ -588,6 +622,22 @@ body {
|
|
|
588
622
|
background: var(--bg-1, #f5f0e6); border: 1px solid var(--border, #d4cbb8); border-radius: 8px;
|
|
589
623
|
padding: 14px 16px; margin-bottom: 14px;
|
|
590
624
|
}
|
|
625
|
+
/* PND-094 ext -- multi-workflow: cada workflow es una card separada. */
|
|
626
|
+
#yf-full .pz-wfl-card { margin-bottom: 26px; padding-bottom: 6px; border-bottom: 1px dashed var(--border, #d4cbb8); }
|
|
627
|
+
#yf-full .pz-wfl-card:last-child { border-bottom: none; }
|
|
628
|
+
/* selector entre workflows abiertos (solo con >1). */
|
|
629
|
+
#yf-full .pz-wfl-sel { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 16px; }
|
|
630
|
+
#yf-full .pz-wfl-chip {
|
|
631
|
+
font-size: 12px; color: var(--ink-muted, #6f6650); cursor: pointer;
|
|
632
|
+
background: var(--bg-1, #f5f0e6); border: 1px solid var(--border, #d4cbb8);
|
|
633
|
+
border-radius: 14px; padding: 4px 12px; font-family: inherit;
|
|
634
|
+
}
|
|
635
|
+
#yf-full .pz-wfl-chip:hover { border-color: var(--indigo, #4f5b87); }
|
|
636
|
+
#yf-full .pz-wfl-chip.active {
|
|
637
|
+
color: #fff; background: var(--indigo, #4f5b87); border-color: var(--indigo, #4f5b87); font-weight: 600;
|
|
638
|
+
}
|
|
639
|
+
#yf-full .pz-wfl-adhoc { color: var(--amber, #c8a04d); font-weight: 700; }
|
|
640
|
+
#yf-full .pz-wfl-empty-stage { opacity: 0.5; font-style: italic; }
|
|
591
641
|
#yf-full .pz-wfl-title { font-family: var(--font-kanji, "Noto Serif JP", serif); font-size: 18px; color: var(--ink, #1a1a1a); }
|
|
592
642
|
#yf-full .pz-wfl-kind {
|
|
593
643
|
display: inline-block; font-size: 11px; color: var(--indigo, #4f5b87);
|
|
@@ -624,7 +674,7 @@ body {
|
|
|
624
674
|
#yf-full .pz-wfl-hang { position: relative; margin: 0 0 0 14px; padding-top: 12px; }
|
|
625
675
|
#yf-full .pz-wfl-hang::before { content: ''; position: absolute; left: 0; top: -2px; bottom: 12px; width: 2px; background: #d4cbb8; }
|
|
626
676
|
#yf-full .pz-wfl-task {
|
|
627
|
-
position: relative; display: flex; align-items:
|
|
677
|
+
position: relative; display: flex; align-items: flex-start; gap: 6px;
|
|
628
678
|
background: var(--bg-1, #f5f0e6); border: 1px solid var(--border, #d4cbb8); border-radius: 6px;
|
|
629
679
|
padding: 5px 8px; margin-bottom: 7px; font-size: 12px; color: var(--ink-muted, #6f6650);
|
|
630
680
|
}
|
|
@@ -634,7 +684,14 @@ body {
|
|
|
634
684
|
#yf-full .pz-wfl-check { width: 14px; text-align: center; color: #c8b89a; flex: 0 0 auto; }
|
|
635
685
|
#yf-full .pz-wfl-task.wfl-done .pz-wfl-check { color: #6f8b5e; }
|
|
636
686
|
#yf-full .pz-wfl-task.wfl-active .pz-wfl-check { color: var(--amber, #c8a04d); }
|
|
637
|
-
|
|
687
|
+
/* el titulo es el item flexible: min-width:0 + word-break para que una
|
|
688
|
+
* palabra larga (p.ej. MobileManagedBrainClient) corte dentro de la card
|
|
689
|
+
* y no se desborde por la derecha. */
|
|
690
|
+
#yf-full .pz-wfl-task-title { flex: 1 1 auto; min-width: 0; line-height: 1.2; overflow-wrap: anywhere; word-break: break-word; }
|
|
691
|
+
/* Pestana BPMN 2.0 (flujos de proceso). */
|
|
692
|
+
#yf-full .pz-body.pz-bpmn-wrap { background: var(--bg-1, #f5f0e6); padding: 12px; overflow: auto; }
|
|
693
|
+
#yf-full .pz-bpmn-svg { display: block; width: 100%; height: auto; }
|
|
694
|
+
#yf-full .pz-bpmn-empty { color: var(--ink-muted, #8a7f68); font-style: italic; padding: 28px; text-align: center; }
|
|
638
695
|
#yf-full .pz-body a {
|
|
639
696
|
color: #2563eb; text-decoration: underline;
|
|
640
697
|
}
|
|
@@ -813,6 +870,26 @@ body {
|
|
|
813
870
|
.header .brand .t { display: flex; flex-direction: column; line-height: 1.1; }
|
|
814
871
|
.header .brand .t .name { font-size: 14px; font-weight: 600; }
|
|
815
872
|
.header .brand .t .proj { font-size: 11px; opacity: 0.8; }
|
|
873
|
+
.header .brand .t .proj-switch { cursor: pointer; display: inline-flex; align-items: center;
|
|
874
|
+
align-self: flex-start; gap: 4px; padding: 1px 6px; margin-left: -4px; border-radius: 6px;
|
|
875
|
+
border: 1px solid transparent; transition: background 0.12s, border-color 0.12s; }
|
|
876
|
+
.header .brand .t .proj-switch:hover,
|
|
877
|
+
.header .brand .t .proj-switch:focus-visible {
|
|
878
|
+
opacity: 1; background: rgba(255,255,255,0.16); border-color: rgba(255,255,255,0.28); outline: none; }
|
|
879
|
+
.proj-menu { position: fixed; z-index: 9999; min-width: 240px; max-width: 420px;
|
|
880
|
+
max-height: 60vh; overflow-y: auto; background: white; color: var(--ink);
|
|
881
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
882
|
+
box-shadow: 0 6px 24px rgba(0,0,0,0.18); padding: 4px; }
|
|
883
|
+
.proj-menu .proj-menu-title { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
|
|
884
|
+
color: var(--ink-muted); padding: 6px 8px 2px; }
|
|
885
|
+
.proj-menu .proj-row { display: flex; flex-direction: column; gap: 1px;
|
|
886
|
+
padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
887
|
+
.proj-menu .proj-row:hover { background: var(--bg2, #f1efe7); }
|
|
888
|
+
.proj-menu .proj-row.on { background: rgba(80,90,160,0.08); }
|
|
889
|
+
.proj-menu .proj-row .proj-row-name { font-size: 13px; font-weight: 600; }
|
|
890
|
+
.proj-menu .proj-row .proj-row-path { font-size: 10px; color: var(--ink-muted);
|
|
891
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
892
|
+
.proj-menu .proj-add { color: var(--indigo); font-weight: 600; border-top: 1px solid var(--border-subtle); margin-top: 2px; }
|
|
816
893
|
.header .actions { display: flex; gap: 4px; }
|
|
817
894
|
.header .actions button {
|
|
818
895
|
background: rgba(255,255,255,0.1); color: white;
|
|
@@ -1263,7 +1340,7 @@ body {
|
|
|
1263
1340
|
/* ---- input bar ---- */
|
|
1264
1341
|
.bar { border-top: 1px solid var(--border-subtle); background: white;
|
|
1265
1342
|
padding: 8px; display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
|
1266
|
-
.bar input { flex: 1; padding: 8px 10px;
|
|
1343
|
+
.bar input { flex: 1; min-width: 0; padding: 8px 10px;
|
|
1267
1344
|
border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-family: inherit; }
|
|
1268
1345
|
.bar input:focus { outline: none; border-color: var(--indigo); box-shadow: 0 0 0 3px rgba(79,91,135,0.12); }
|
|
1269
1346
|
.bar .mic, .bar .send {
|
|
@@ -1284,9 +1361,25 @@ body {
|
|
|
1284
1361
|
cursor: pointer; font-size: 14px;
|
|
1285
1362
|
}
|
|
1286
1363
|
.bar .tts-toggle.on { background: var(--indigo); color: white; }
|
|
1287
|
-
.bar .send { background: var(--indigo); padding: 0
|
|
1288
|
-
font-size:
|
|
1364
|
+
.bar .send { background: var(--indigo); padding: 0 9px; width: auto; min-width: 0;
|
|
1365
|
+
flex-shrink: 0; white-space: nowrap; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
|
1289
1366
|
.bar .send:disabled { background: #aaa; cursor: not-allowed; }
|
|
1367
|
+
.bar .stop { background: var(--err, #c8472e); color: white; padding: 0 9px; width: auto;
|
|
1368
|
+
min-width: 0; flex-shrink: 0; white-space: nowrap; border-radius: 6px; font-size: 12px;
|
|
1369
|
+
font-weight: 600; border: none; cursor: pointer; height: 36px; }
|
|
1370
|
+
|
|
1371
|
+
/* ---- message queue (task #28) ---- */
|
|
1372
|
+
.queue { display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
1373
|
+
padding: 6px 12px 0; }
|
|
1374
|
+
.queue .queue-label { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
|
|
1375
|
+
color: var(--ink-muted); margin-right: 2px; }
|
|
1376
|
+
.queue .queue-chip { display: inline-flex; align-items: center; gap: 5px;
|
|
1377
|
+
background: var(--bg2, #f1efe7); border: 1px solid var(--border); border-radius: 12px;
|
|
1378
|
+
padding: 2px 9px; font-size: 12px; color: var(--ink); cursor: pointer; max-width: 240px;
|
|
1379
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1380
|
+
.queue .queue-chip:hover { border-color: var(--err, #c8472e); }
|
|
1381
|
+
.queue .queue-x { color: var(--ink-muted); font-weight: 700; }
|
|
1382
|
+
.queue .queue-chip:hover .queue-x { color: var(--err, #c8472e); }
|
|
1290
1383
|
|
|
1291
1384
|
/* ---- status row ---- */
|
|
1292
1385
|
.status { font-size: 11px; color: var(--ink-muted); padding: 4px 12px; text-align: center; flex-shrink: 0; }
|
|
@@ -1713,7 +1806,7 @@ ${renderYujinSplash()}
|
|
|
1713
1806
|
<div class="header">
|
|
1714
1807
|
<div class="brand">
|
|
1715
1808
|
<span class="k">${escapeHtml(tr.brandKanji)}</span>
|
|
1716
|
-
<div class="t"><span class="name">Yujin Forge</span><span class="proj" id="proj-mini" data-nac-id="yujin.panel.project-mini"></span></div>
|
|
1809
|
+
<div class="t"><span class="name">Yujin Forge</span><span class="proj proj-switch" id="proj-mini" data-proj-switch role="button" tabindex="0" title="${escapeHtml(tr.projectSwitchHint)}" data-nac-id="yujin.panel.project-mini"></span></div>
|
|
1717
1810
|
</div>
|
|
1718
1811
|
<div class="actions">
|
|
1719
1812
|
<button class="keys-btn" data-vault-open aria-label="${escapeHtml(tr.keysButton)}" title="${escapeHtml(tr.keysButton)}">${escapeHtml(tr.keysButton)}</button>
|
|
@@ -1723,12 +1816,14 @@ ${renderYujinSplash()}
|
|
|
1723
1816
|
</div>
|
|
1724
1817
|
<div id="stream-mini" data-nac-id="yujin.chat.stream-mini" class="stream"></div>
|
|
1725
1818
|
<div class="status" id="status-mini" data-nac-id="yujin.chat.status-mini"></div>
|
|
1819
|
+
<div id="queue-mini" class="queue" style="display:none"></div>
|
|
1726
1820
|
<form id="bar-mini" data-nac-id="yujin.chat.bar-mini" class="bar" onsubmit="return false">
|
|
1727
1821
|
<button type="button" class="mic" data-mic data-mic-btn aria-label="${escapeHtml(tr.micButton)}" title="${escapeHtml(tr.micButton)}">🎙</button>
|
|
1728
1822
|
<button type="button" class="tts-toggle" data-tts-toggle aria-label="${escapeHtml(tr.ttsButton)}" title="${escapeHtml(tr.ttsButton)}">🔊</button>
|
|
1729
1823
|
<button type="button" class="clip" data-attach-clip aria-label="Adjuntar archivo" title="Adjuntar archivo">📎</button>
|
|
1730
1824
|
<input type="text" placeholder="${escapeHtml(tr.inputPlaceholder)}" aria-label="${escapeHtml(tr.inputPlaceholder)}" autocomplete="off">
|
|
1731
1825
|
<button type="submit" class="send">${escapeHtml(tr.sendButton)}</button>
|
|
1826
|
+
<button type="button" class="stop" data-stop aria-label="${escapeHtml(tr.stopButton)}" title="${escapeHtml(tr.stopButton)}" style="display:none">■ ${escapeHtml(tr.stopButton)}</button>
|
|
1732
1827
|
</form>
|
|
1733
1828
|
<input type="file" id="attach-file-input" multiple style="display:none" data-nac-id="yujin.chat.attach-input">
|
|
1734
1829
|
</div>
|
|
@@ -1883,7 +1978,7 @@ ${renderYujinSplash()}
|
|
|
1883
1978
|
<div class="header">
|
|
1884
1979
|
<div class="brand">
|
|
1885
1980
|
<span class="k">${escapeHtml(tr.brandKanji)}</span>
|
|
1886
|
-
<div class="t"><span class="name">Yujin Forge</span><span class="proj" id="proj-full" data-nac-id="yujin.panel.project-full"></span></div>
|
|
1981
|
+
<div class="t"><span class="name">Yujin Forge</span><span class="proj proj-switch" id="proj-full" data-proj-switch role="button" tabindex="0" title="${escapeHtml(tr.projectSwitchHint)}" data-nac-id="yujin.panel.project-full"></span></div>
|
|
1887
1982
|
</div>
|
|
1888
1983
|
<div class="actions">
|
|
1889
1984
|
<button class="agents-btn"
|
|
@@ -2343,18 +2438,17 @@ ${renderYujinSplash()}
|
|
|
2343
2438
|
</div>
|
|
2344
2439
|
</div>
|
|
2345
2440
|
|
|
2346
|
-
<!-- Vincular telefono (dogfood 2026-06-10
|
|
2347
|
-
|
|
2441
|
+
<!-- Vincular telefono (dogfood 2026-06-10; Option 3 2026-06-16 --
|
|
2442
|
+
code-less: el QR es autocontenido, se escanea y listo). -->
|
|
2348
2443
|
<div class="set-section">
|
|
2349
2444
|
<div class="set-section-title">Vincular telefono</div>
|
|
2350
2445
|
<div class="set-row">
|
|
2351
|
-
<span class="set-lbl" style="opacity:0.7;font-size:11px;">Abri Forge Mobile, escanea este QR
|
|
2446
|
+
<span class="set-lbl" style="opacity:0.7;font-size:11px;">Abri Forge Mobile, toca "Vincular" y escanea este QR. Listo.</span>
|
|
2352
2447
|
<button type="button" class="set-btn" id="set-pair-generate">Generar QR</button>
|
|
2353
2448
|
</div>
|
|
2354
2449
|
<div id="set-pair-result" class="hidden" style="text-align:center;padding:8px 0;">
|
|
2355
|
-
<div id="set-pair-qr" style="display:inline-block;background:#
|
|
2356
|
-
<div
|
|
2357
|
-
<div id="set-pair-expiry" style="font-size:11px;opacity:0.6;margin-top:2px;"></div>
|
|
2450
|
+
<div id="set-pair-qr" style="display:inline-block;background:#ffffff;padding:10px;border-radius:8px;width:220px;height:220px;"></div>
|
|
2451
|
+
<div id="set-pair-expiry" style="font-size:11px;opacity:0.6;margin-top:6px;"></div>
|
|
2358
2452
|
</div>
|
|
2359
2453
|
<div id="set-pair-error" class="hidden" style="font-size:12px;color:var(--err,#c0392b);padding:6px 0;"></div>
|
|
2360
2454
|
</div>
|
|
@@ -2472,12 +2566,14 @@ ${renderYujinSplash()}
|
|
|
2472
2566
|
<div class="chat-col">
|
|
2473
2567
|
<div id="stream-full" data-nac-id="yujin.chat.stream-full" class="stream"></div>
|
|
2474
2568
|
<div class="status" id="status-full" data-nac-id="yujin.chat.status-full"></div>
|
|
2569
|
+
<div id="queue-full" class="queue" style="display:none"></div>
|
|
2475
2570
|
<form id="bar-full" data-nac-id="yujin.chat.bar-full" class="bar" onsubmit="return false">
|
|
2476
2571
|
<button type="button" class="mic" data-mic data-mic-btn aria-label="${escapeHtml(tr.micButton)}" title="${escapeHtml(tr.micButton)}">🎙</button>
|
|
2477
2572
|
<button type="button" class="tts-toggle" data-tts-toggle aria-label="${escapeHtml(tr.ttsButton)}" title="${escapeHtml(tr.ttsButton)}">🔊</button>
|
|
2478
2573
|
<button type="button" class="clip" data-attach-clip aria-label="Adjuntar archivo" title="Adjuntar archivo">📎</button>
|
|
2479
2574
|
<input type="text" placeholder="${escapeHtml(tr.inputPlaceholder)}" aria-label="${escapeHtml(tr.inputPlaceholder)}" autocomplete="off">
|
|
2480
2575
|
<button type="submit" class="send">${escapeHtml(tr.sendButton)}</button>
|
|
2576
|
+
<button type="button" class="stop" data-stop aria-label="${escapeHtml(tr.stopButton)}" title="${escapeHtml(tr.stopButton)}" style="display:none">■ ${escapeHtml(tr.stopButton)}</button>
|
|
2481
2577
|
</form>
|
|
2482
2578
|
</div>
|
|
2483
2579
|
<div class="pizarra" data-nac-id="yujin.pizarron" data-nac-role="region">
|
|
@@ -2601,9 +2697,109 @@ function showMode(mode) {
|
|
|
2601
2697
|
}
|
|
2602
2698
|
|
|
2603
2699
|
function setProj() {
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2700
|
+
/* Project selector (task #33) -- render the name + a caret so it reads
|
|
2701
|
+
* as a dropdown (not a passive version stamp). The version moves to the
|
|
2702
|
+
* tooltip. */
|
|
2703
|
+
const label = CONFIG.projectName + ' ▾';
|
|
2704
|
+
const title = CONFIG.projectName + ' v' + CONFIG.version + ' — ' + (CONFIG.i18n.projectSwitchHint || '');
|
|
2705
|
+
for (const id of ['#proj-mini', '#proj-full']) {
|
|
2706
|
+
const el = $(id);
|
|
2707
|
+
if (!el) continue;
|
|
2708
|
+
el.textContent = label;
|
|
2709
|
+
el.setAttribute('title', title);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/* Project selector (task #33) -- the project label in the header is a
|
|
2714
|
+
* switcher. Clicking it lists every known project (~/.yujin-forge/
|
|
2715
|
+
* projects.json) and switching HOT-reanchors the live session: the
|
|
2716
|
+
* pizarron, graph + startup tabs follow immediately. */
|
|
2717
|
+
function closeProjectMenu() {
|
|
2718
|
+
const m = document.getElementById('proj-menu');
|
|
2719
|
+
if (m && m.parentNode) m.parentNode.removeChild(m);
|
|
2720
|
+
document.removeEventListener('click', onProjMenuOutside, true);
|
|
2721
|
+
}
|
|
2722
|
+
function onProjMenuOutside(e) {
|
|
2723
|
+
const m = document.getElementById('proj-menu');
|
|
2724
|
+
if (m && !m.contains(e.target) && !(e.target.closest && e.target.closest('[data-proj-switch]'))) {
|
|
2725
|
+
closeProjectMenu();
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async function openProjectMenu(anchor) {
|
|
2729
|
+
if (document.getElementById('proj-menu')) { closeProjectMenu(); return; }
|
|
2730
|
+
let reg = { projects: [], active_slug: null };
|
|
2731
|
+
try {
|
|
2732
|
+
const r = await fetch('/api/forge/projects');
|
|
2733
|
+
const j = await r.json();
|
|
2734
|
+
if (j && j.ok && j.registry) reg = j.registry;
|
|
2735
|
+
else if (j && Array.isArray(j.projects)) reg = j;
|
|
2736
|
+
} catch (e) { /* show just the add option */ }
|
|
2737
|
+
const menu = document.createElement('div');
|
|
2738
|
+
menu.id = 'proj-menu';
|
|
2739
|
+
menu.className = 'proj-menu';
|
|
2740
|
+
const rows = (reg.projects || []).slice().sort(function (a, b) {
|
|
2741
|
+
return String(b.last_active_at || '').localeCompare(String(a.last_active_at || ''));
|
|
2742
|
+
}).map(function (p) {
|
|
2743
|
+
const active = p.slug === reg.active_slug;
|
|
2744
|
+
return '<div class="proj-row' + (active ? ' on' : '') + '" data-proj-slug="' + escapeHtml(p.slug) + '" role="button" tabindex="0">'
|
|
2745
|
+
+ '<span class="proj-row-name">' + (active ? '● ' : '') + escapeHtml(p.slug) + '</span>'
|
|
2746
|
+
+ '<span class="proj-row-path">' + escapeHtml(String(p.path || '')) + '</span>'
|
|
2747
|
+
+ '</div>';
|
|
2748
|
+
}).join('');
|
|
2749
|
+
menu.innerHTML = '<div class="proj-menu-title">' + escapeHtml(CONFIG.i18n.projectTitle) + '</div>'
|
|
2750
|
+
+ (rows || '')
|
|
2751
|
+
+ '<div class="proj-row proj-add" data-proj-add role="button" tabindex="0">' + escapeHtml(CONFIG.i18n.projectAdd) + '</div>';
|
|
2752
|
+
document.body.appendChild(menu);
|
|
2753
|
+
/* position under the anchor */
|
|
2754
|
+
try {
|
|
2755
|
+
const rect = anchor.getBoundingClientRect();
|
|
2756
|
+
menu.style.top = (rect.bottom + 4) + 'px';
|
|
2757
|
+
menu.style.left = Math.max(8, rect.left) + 'px';
|
|
2758
|
+
} catch (e) { /* default top-left */ }
|
|
2759
|
+
menu.querySelectorAll('[data-proj-slug]').forEach(function (row) {
|
|
2760
|
+
row.addEventListener('click', function () { switchProject(row.getAttribute('data-proj-slug')); });
|
|
2761
|
+
});
|
|
2762
|
+
const addRow = menu.querySelector('[data-proj-add]');
|
|
2763
|
+
if (addRow) addRow.addEventListener('click', addProjectViaPrompt);
|
|
2764
|
+
setTimeout(function () { document.addEventListener('click', onProjMenuOutside, true); }, 0);
|
|
2765
|
+
}
|
|
2766
|
+
async function switchProject(slug) {
|
|
2767
|
+
if (!slug) return;
|
|
2768
|
+
closeProjectMenu();
|
|
2769
|
+
setStatus(CONFIG.i18n.projectSwitching);
|
|
2770
|
+
try {
|
|
2771
|
+
const r = await fetch('/api/forge/projects/active', {
|
|
2772
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
2773
|
+
body: JSON.stringify({ slug: slug }),
|
|
2774
|
+
});
|
|
2775
|
+
const j = await r.json();
|
|
2776
|
+
if (j && j.ok) {
|
|
2777
|
+
/* optimistic label update; the system.reanchor event reloads the
|
|
2778
|
+
* startup tabs + the graph follows server-side. */
|
|
2779
|
+
CONFIG.projectName = slug;
|
|
2780
|
+
setProj();
|
|
2781
|
+
setStatus('');
|
|
2782
|
+
} else {
|
|
2783
|
+
setStatus((j && j.error) || 'No se pudo cambiar de proyecto.', true);
|
|
2784
|
+
}
|
|
2785
|
+
} catch (e) {
|
|
2786
|
+
setStatus('No se pudo cambiar de proyecto.', true);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
async function addProjectViaPrompt() {
|
|
2790
|
+
closeProjectMenu();
|
|
2791
|
+
const path = prompt(CONFIG.i18n.projectAddPrompt, '');
|
|
2792
|
+
if (!path || !path.trim()) return;
|
|
2793
|
+
try {
|
|
2794
|
+
const r = await fetch('/api/forge/projects/add', {
|
|
2795
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
2796
|
+
body: JSON.stringify({ path: path.trim() }),
|
|
2797
|
+
});
|
|
2798
|
+
const j = await r.json();
|
|
2799
|
+
if (!j || !j.ok) { setStatus((j && j.error) || 'No se pudo anadir el proyecto.', true); return; }
|
|
2800
|
+
/* reopen the menu so the user can pick the freshly added project */
|
|
2801
|
+
openProjectMenu($('#proj-full') || $('#proj-mini'));
|
|
2802
|
+
} catch (e) { setStatus('No se pudo anadir el proyecto.', true); }
|
|
2607
2803
|
}
|
|
2608
2804
|
|
|
2609
2805
|
function escapeHtml(s) {
|
|
@@ -3524,10 +3720,8 @@ async function pairGenerate() {
|
|
|
3524
3720
|
const j = await r.json();
|
|
3525
3721
|
if (j && j.ok) {
|
|
3526
3722
|
const qr = document.getElementById('set-pair-qr');
|
|
3527
|
-
const code = document.getElementById('set-pair-code');
|
|
3528
3723
|
const exp = document.getElementById('set-pair-expiry');
|
|
3529
3724
|
if (qr) qr.innerHTML = j.qr_svg || '';
|
|
3530
|
-
if (code) code.textContent = j.code || '';
|
|
3531
3725
|
if (exp && j.expires_at) {
|
|
3532
3726
|
const mins = Math.max(0, Math.round((new Date(j.expires_at).getTime() - Date.now()) / 60000));
|
|
3533
3727
|
exp.textContent = 'Expira en ~' + mins + ' min. Generalo de nuevo si caduca.';
|
|
@@ -4677,8 +4871,23 @@ function renderPizarronCardBody(it) {
|
|
|
4677
4871
|
* estetica sumi-e. El modelo manda markdown (seguro: no se
|
|
4678
4872
|
* ejecuta HTML/JS), el panel le da el look de documento. */
|
|
4679
4873
|
if (it.kind === 'doc') {
|
|
4874
|
+
/* Default de documentos = HTML (pedido de Pablo). El brain genera
|
|
4875
|
+
* HTML por defecto; se renderiza sanitizado DENTRO de la hoja Yujin.
|
|
4876
|
+
* Backward-compat: si el content es markdown (no empieza con un tag
|
|
4877
|
+
* de bloque), cae a renderMdRich, asi los docs viejos siguen bien. */
|
|
4878
|
+
const dc = String(it.content || '');
|
|
4879
|
+
/* Pablo 2026-06-15 -- el heuristico viejo solo miraba si EMPEZABA
|
|
4880
|
+
* con un tag de bloque, asi que un doc HTML con un \n o una linea
|
|
4881
|
+
* de texto antes del primer tag caia a markdown y se veia "feo"
|
|
4882
|
+
* (tags como texto). Ahora: es HTML si empieza con un tag de bloque
|
|
4883
|
+
* O si contiene un tag de CIERRE de bloque en cualquier parte (senal
|
|
4884
|
+
* fuerte que el markdown no tiene). */
|
|
4885
|
+
const looksHtml =
|
|
4886
|
+
/^\\s*<(?:!doctype|html|head|body|div|section|article|main|header|h[1-6]|p|ul|ol|table|figure|blockquote|pre)\\b/i.test(dc)
|
|
4887
|
+
|| /<\\/(?:h[1-6]|div|section|article|main|header|body|html|p|ul|ol|li|table|tr|td|figure|blockquote|pre)\\s*>/i.test(dc);
|
|
4888
|
+
const inner = looksHtml ? sanitizeHtml(dc) : renderMdRich(dc);
|
|
4680
4889
|
return '<div class="pz-body pz-doc-wrap" data-nac-id="yujin.pizarron.item.' + it.id + '.body" data-nac-role="region" data-nac-action="scroll" tabindex="0">'
|
|
4681
|
-
+ '<article class="pz-doc-page pz-sumi">' +
|
|
4890
|
+
+ '<article class="pz-doc-page pz-sumi">' + inner + '</article>'
|
|
4682
4891
|
+ '</div>';
|
|
4683
4892
|
}
|
|
4684
4893
|
/* Lienzo Coding (dogfood 2026-06-10) -- mientras el brain codea
|
|
@@ -4697,6 +4906,17 @@ function renderPizarronCardBody(it) {
|
|
|
4697
4906
|
: '<div class="pz-coding-detail" data-pz-coding-detail>' + renderCodingDetail() + '</div>')
|
|
4698
4907
|
+ '</div>';
|
|
4699
4908
|
}
|
|
4909
|
+
/* PND-007 -- Tareas: tareas de fondo que sobreviven al turno. Se
|
|
4910
|
+
* llena en vivo en initTasksTab() (poll a /api/forge/task). */
|
|
4911
|
+
if (it.kind === 'tasks') {
|
|
4912
|
+
return '<div class="pz-body pz-tasks-wrap pz-sumi" data-nac-id="yujin.pizarron.item.' + it.id + '.body" data-nac-role="region" tabindex="0">'
|
|
4913
|
+
+ '<div class="pz-tasks-new">'
|
|
4914
|
+
+ '<input type="text" class="pz-tasks-input" data-pz-tasks-cmd placeholder="comando (ej: pnpm install)..." />'
|
|
4915
|
+
+ '<button type="button" class="pz-tasks-run" data-pz-tasks-run>Lanzar</button>'
|
|
4916
|
+
+ '</div>'
|
|
4917
|
+
+ '<div class="pz-tasks-list" data-pz-tasks-list><div class="pz-tasks-empty">cargando...</div></div>'
|
|
4918
|
+
+ '</div>';
|
|
4919
|
+
}
|
|
4700
4920
|
/* PND-094 -- vista de workflow: fases con nombre (nivel 1) + sus
|
|
4701
4921
|
* tareas debajo (nivel 2), con avance por tarea y por fase. */
|
|
4702
4922
|
if (it.kind === 'workflow') {
|
|
@@ -4728,9 +4948,15 @@ function renderPizarronCardBody(it) {
|
|
|
4728
4948
|
}
|
|
4729
4949
|
if (it.kind === 'bpmn' || it.kind === 'flow') {
|
|
4730
4950
|
const trimmed = it.content.trim();
|
|
4951
|
+
/* SVG crudo -> passthrough (compat). */
|
|
4731
4952
|
if (trimmed.startsWith('<svg')) {
|
|
4732
4953
|
return bodyOpen + sanitizeHtml(trimmed) + '</div>';
|
|
4733
4954
|
}
|
|
4955
|
+
/* Modelo JSON {nodes,flows,lanes?} -> renderer BPMN 2.0 propio. */
|
|
4956
|
+
if (trimmed.charAt(0) === '{') {
|
|
4957
|
+
return '<div class="pz-body pz-bpmn-wrap" data-nac-id="yujin.pizarron.item.' + it.id + '.body" data-nac-role="region" data-nac-action="scroll" tabindex="0">'
|
|
4958
|
+
+ renderBpmnView(trimmed) + '</div>';
|
|
4959
|
+
}
|
|
4734
4960
|
return bodyOpen + '<pre class="pz-code"><code>' + escapeHtml(it.content) + '</code></pre></div>';
|
|
4735
4961
|
}
|
|
4736
4962
|
return bodyOpen + '<pre><code>' + escapeHtml(it.content) + '</code></pre></div>';
|
|
@@ -4821,6 +5047,188 @@ function renderPizarron() {
|
|
|
4821
5047
|
if (it.kind === 'coding') {
|
|
4822
5048
|
try { initCodingTab(it); } catch (e) { /* render best-effort */ }
|
|
4823
5049
|
}
|
|
5050
|
+
if (it.kind === 'tasks') {
|
|
5051
|
+
try { initTasksTab(it); } catch (e) { /* render best-effort */ }
|
|
5052
|
+
}
|
|
5053
|
+
if (it.kind === 'workflow') {
|
|
5054
|
+
try { initWorkflowTab(it); } catch (e) { /* render best-effort */ }
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
/* Selector multi-workflow: persiste el indice elegido por tab id (igual
|
|
5059
|
+
* que el filtro del grafo) para que un refresh del board no resetee la
|
|
5060
|
+
* seleccion. Con un solo workflow no hay barra de chips: no-op. */
|
|
5061
|
+
var _wflSelByTab = {};
|
|
5062
|
+
function initWorkflowTab(it) {
|
|
5063
|
+
var body = document.querySelector('[data-nac-id="yujin.pizarron.item.' + it.id + '.body"]');
|
|
5064
|
+
if (!body) return;
|
|
5065
|
+
var root = body.querySelector('[data-pz-wfl-root]');
|
|
5066
|
+
if (!root) return;
|
|
5067
|
+
var chips = root.querySelectorAll('[data-pz-wfl-chip]');
|
|
5068
|
+
var cards = root.querySelectorAll('[data-pz-wfl-card]');
|
|
5069
|
+
if (chips.length <= 1) return; /* un solo workflow: sin selector */
|
|
5070
|
+
var show = function (idx) {
|
|
5071
|
+
for (var i = 0; i < cards.length; i++) {
|
|
5072
|
+
cards[i].style.display = (String(cards[i].getAttribute('data-wfl-idx')) === String(idx)) ? '' : 'none';
|
|
5073
|
+
}
|
|
5074
|
+
for (var j = 0; j < chips.length; j++) {
|
|
5075
|
+
var on = String(chips[j].getAttribute('data-wfl-idx')) === String(idx);
|
|
5076
|
+
if (on) chips[j].classList.add('active'); else chips[j].classList.remove('active');
|
|
5077
|
+
}
|
|
5078
|
+
_wflSelByTab[it.id] = String(idx);
|
|
5079
|
+
};
|
|
5080
|
+
for (var k = 0; k < chips.length; k++) {
|
|
5081
|
+
chips[k].addEventListener('click', function () { show(this.getAttribute('data-wfl-idx')); });
|
|
5082
|
+
}
|
|
5083
|
+
var saved = _wflSelByTab[it.id];
|
|
5084
|
+
show(saved != null ? saved : '0');
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
/* ====================================================================
|
|
5088
|
+
* PND-007 -- Tareas tab. Polls /api/forge/task; renders survivable
|
|
5089
|
+
* background tasks with a per-type icon, status, a log peek + kill, and
|
|
5090
|
+
* a small launcher. Each task TYPE (install/build/server/...) gets its
|
|
5091
|
+
* own sumi-e glyph.
|
|
5092
|
+
* ================================================================== */
|
|
5093
|
+
var yfTasksTimer = null;
|
|
5094
|
+
|
|
5095
|
+
function taskTypeIcon(type) {
|
|
5096
|
+
switch (String(type || 'other')) {
|
|
5097
|
+
case 'install': return { g: '↓', c: '#4f5b87' }; /* down arrow */
|
|
5098
|
+
case 'build': return { g: '⚒', c: '#c8a04d' }; /* hammer+pick */
|
|
5099
|
+
case 'server': return { g: '◉', c: '#3fa85e' }; /* fisheye */
|
|
5100
|
+
case 'test': return { g: '✓', c: '#6f8b5e' }; /* check */
|
|
5101
|
+
case 'deploy': return { g: '☁', c: '#8a7bb0' }; /* cloud */
|
|
5102
|
+
case 'git': return { g: '⎇', c: '#b07b4f' }; /* alternation */
|
|
5103
|
+
case 'script': return { g: '›', c: '#7a8aa0' }; /* chevron */
|
|
5104
|
+
default: return { g: '●', c: '#9a9a9a' }; /* disc */
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
function taskStatusBadge(s) {
|
|
5109
|
+
if (s === 'running') return { t: 'corriendo', c: '#c8a04d' };
|
|
5110
|
+
if (s === 'done') return { t: 'terminada', c: '#6f8b5e' };
|
|
5111
|
+
if (s === 'failed') return { t: 'fallo', c: '#c8472e' };
|
|
5112
|
+
if (s === 'killed') return { t: 'detenida', c: '#9a9a9a' };
|
|
5113
|
+
if (s === 'exited') return { t: 'salio', c: '#9a9a9a' };
|
|
5114
|
+
return { t: String(s || ''), c: '#9a9a9a' };
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
function renderTasksList(tasks) {
|
|
5118
|
+
if (!tasks || tasks.length === 0) {
|
|
5119
|
+
return '<div class="pz-tasks-empty">Sin tareas de fondo. Lo que lances aca sobrevive al turno.</div>';
|
|
5120
|
+
}
|
|
5121
|
+
var html = '';
|
|
5122
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
5123
|
+
var t = tasks[i];
|
|
5124
|
+
var ic = taskTypeIcon(t.type);
|
|
5125
|
+
var sb = taskStatusBadge(t.status);
|
|
5126
|
+
var cmdline = escapeHtml(String(t.cmd || '') + (t.args && t.args.length ? ' ' + t.args.join(' ') : '')).slice(0, 200);
|
|
5127
|
+
html += '<div class="pz-task" data-pz-task="' + escapeHtml(t.id) + '">'
|
|
5128
|
+
+ '<div class="pz-task-ic" style="color:' + ic.c + '">' + ic.g + '</div>'
|
|
5129
|
+
+ '<div class="pz-task-main">'
|
|
5130
|
+
+ '<div class="pz-task-label">' + escapeHtml(String(t.label || t.cmd || 'tarea')) + '</div>'
|
|
5131
|
+
+ '<div class="pz-task-cmd">' + cmdline + '</div>'
|
|
5132
|
+
+ '</div>'
|
|
5133
|
+
+ '<div class="pz-task-side">'
|
|
5134
|
+
+ '<span class="pz-task-badge" style="color:' + sb.c + ';border-color:' + sb.c + '">' + sb.t + '</span>'
|
|
5135
|
+
+ (t.status === 'running'
|
|
5136
|
+
? '<button type="button" class="pz-task-kill" data-pz-task-kill="' + escapeHtml(t.id) + '">stop</button>'
|
|
5137
|
+
: '<button type="button" class="pz-task-log" data-pz-task-log="' + escapeHtml(t.id) + '">log</button>')
|
|
5138
|
+
+ '<button type="button" class="pz-task-del" data-pz-task-del="' + escapeHtml(t.id) + '" title="Eliminar tarea" aria-label="Eliminar tarea">×</button>'
|
|
5139
|
+
+ '</div>'
|
|
5140
|
+
+ '</div>'
|
|
5141
|
+
+ '<div class="pz-task-logbox" data-pz-task-logbox="' + escapeHtml(t.id) + '"></div>';
|
|
5142
|
+
}
|
|
5143
|
+
return html;
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
async function refreshTasksTab() {
|
|
5147
|
+
var list = document.querySelector('[data-pz-tasks-list]');
|
|
5148
|
+
if (!list) return; /* tab closed -> stop touching the DOM */
|
|
5149
|
+
try {
|
|
5150
|
+
var r = await fetch('/api/forge/task');
|
|
5151
|
+
var data = await r.json();
|
|
5152
|
+
if (data && data.ok) list.innerHTML = renderTasksList(data.tasks || []);
|
|
5153
|
+
} catch (e) { /* best-effort */ }
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
async function tasksTabLaunch() {
|
|
5157
|
+
var inp = document.querySelector('[data-pz-tasks-cmd]');
|
|
5158
|
+
if (!inp) return;
|
|
5159
|
+
var raw = String(inp.value || '').trim();
|
|
5160
|
+
if (!raw) return;
|
|
5161
|
+
/* split a simple "cmd arg arg" string (no shell parsing).
|
|
5162
|
+
* NOTE: this code rides inside the panel's outer template literal, so
|
|
5163
|
+
* a regex backslash must be DOUBLED -- /\s+/ would be mangled to /s+/
|
|
5164
|
+
* (split on the letter 's': "pnpm install" -> "pnpm in"+"tall"). */
|
|
5165
|
+
var parts = raw.split(/\\s+/);
|
|
5166
|
+
var cmd = parts.shift();
|
|
5167
|
+
try {
|
|
5168
|
+
await fetch('/api/forge/task', {
|
|
5169
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5170
|
+
body: JSON.stringify({ cmd: cmd, args: parts, label: raw.slice(0, 60) }),
|
|
5171
|
+
});
|
|
5172
|
+
inp.value = '';
|
|
5173
|
+
await refreshTasksTab();
|
|
5174
|
+
} catch (e) { /* best-effort */ }
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
async function tasksTabShowLog(id) {
|
|
5178
|
+
var box = document.querySelector('[data-pz-task-logbox="' + id + '"]');
|
|
5179
|
+
if (!box) return;
|
|
5180
|
+
if (box.getAttribute('data-open') === '1') { box.innerHTML = ''; box.removeAttribute('data-open'); return; }
|
|
5181
|
+
try {
|
|
5182
|
+
var r = await fetch('/api/forge/task/log?id=' + encodeURIComponent(id) + '&lines=40');
|
|
5183
|
+
var data = await r.json();
|
|
5184
|
+
box.innerHTML = '<pre class="pz-task-logpre">' + escapeHtml(String((data && data.tail) || '(sin salida)')) + '</pre>';
|
|
5185
|
+
box.setAttribute('data-open', '1');
|
|
5186
|
+
} catch (e) { /* best-effort */ }
|
|
5187
|
+
}
|
|
5188
|
+
|
|
5189
|
+
async function tasksTabKill(id) {
|
|
5190
|
+
try {
|
|
5191
|
+
await fetch('/api/forge/task/kill', {
|
|
5192
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5193
|
+
body: JSON.stringify({ id: id }),
|
|
5194
|
+
});
|
|
5195
|
+
await refreshTasksTab();
|
|
5196
|
+
} catch (e) { /* best-effort */ }
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
async function tasksTabDelete(id) {
|
|
5200
|
+
/* Remove the task from the list (kills it first if still running). */
|
|
5201
|
+
try {
|
|
5202
|
+
await fetch('/api/forge/task/delete', {
|
|
5203
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5204
|
+
body: JSON.stringify({ id: id }),
|
|
5205
|
+
});
|
|
5206
|
+
await refreshTasksTab();
|
|
5207
|
+
} catch (e) { /* best-effort */ }
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
function initTasksTab(it) {
|
|
5211
|
+
var wrap = document.querySelector('.pz-tasks-wrap');
|
|
5212
|
+
if (!wrap) return;
|
|
5213
|
+
var runBtn = wrap.querySelector('[data-pz-tasks-run]');
|
|
5214
|
+
if (runBtn) runBtn.addEventListener('click', function () { tasksTabLaunch().catch(function () {}); });
|
|
5215
|
+
var inp = wrap.querySelector('[data-pz-tasks-cmd]');
|
|
5216
|
+
if (inp) inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') tasksTabLaunch().catch(function () {}); });
|
|
5217
|
+
/* delegated clicks for per-task log/kill */
|
|
5218
|
+
wrap.addEventListener('click', function (e) {
|
|
5219
|
+
var tgt = e.target;
|
|
5220
|
+
if (!tgt || !tgt.getAttribute) return;
|
|
5221
|
+
var killId = tgt.getAttribute('data-pz-task-kill');
|
|
5222
|
+
if (killId) { tasksTabKill(killId).catch(function () {}); return; }
|
|
5223
|
+
var delId = tgt.getAttribute('data-pz-task-del');
|
|
5224
|
+
if (delId) { tasksTabDelete(delId).catch(function () {}); return; }
|
|
5225
|
+
var logId = tgt.getAttribute('data-pz-task-log');
|
|
5226
|
+
if (logId) { tasksTabShowLog(logId).catch(function () {}); return; }
|
|
5227
|
+
});
|
|
5228
|
+
refreshTasksTab().catch(function () {});
|
|
5229
|
+
if (!yfTasksTimer) {
|
|
5230
|
+
yfTasksTimer = setInterval(function () { refreshTasksTab().catch(function () {}); }, 4000);
|
|
5231
|
+
}
|
|
4824
5232
|
}
|
|
4825
5233
|
|
|
4826
5234
|
/* ====================================================================
|
|
@@ -4842,6 +5250,68 @@ function renderPizarron() {
|
|
|
4842
5250
|
function renderWorkflowView(content) {
|
|
4843
5251
|
let v;
|
|
4844
5252
|
try { v = JSON.parse(content || '{}'); } catch (e) { v = null; }
|
|
5253
|
+
/* PND-094 ext -- multi-workflow: content { workflows: [card...] }. Each
|
|
5254
|
+
* card = one coexisting WorkflowInstance with its stages -> ordered
|
|
5255
|
+
* task pendings. Falls through to the legacy single-workflow render
|
|
5256
|
+
* below when content is the old { stages, ... } shape. */
|
|
5257
|
+
if (v && Array.isArray(v.workflows)) {
|
|
5258
|
+
if (v.workflows.length === 0) return '<div class="pz-wfl-empty">Sin workflows abiertos.</div>';
|
|
5259
|
+
var kindLbl = { greenfield: 'App nueva', corrective: 'Correctivo', evolutive: 'Evolutivo' };
|
|
5260
|
+
var ic2 = function (closed) { return closed ? '✓' : '○'; };
|
|
5261
|
+
var multi = v.workflows.length > 1;
|
|
5262
|
+
/* Selector entre los workflows abiertos: con >1 se muestra una barra
|
|
5263
|
+
* de chips (uno por workflow) y SOLO se ve el seleccionado. El estado
|
|
5264
|
+
* lo persiste initWorkflowTab por tab (sobrevive a los refreshes). */
|
|
5265
|
+
var sel = '';
|
|
5266
|
+
if (multi) {
|
|
5267
|
+
sel = '<div class="pz-wfl-sel" data-pz-wfl-sel>';
|
|
5268
|
+
for (var ci = 0; ci < v.workflows.length; ci++) {
|
|
5269
|
+
var cw = v.workflows[ci];
|
|
5270
|
+
sel += '<button type="button" class="pz-wfl-chip' + (ci === 0 ? ' active' : '') + '"'
|
|
5271
|
+
+ ' data-pz-wfl-chip data-wfl-idx="' + ci + '">'
|
|
5272
|
+
+ escapeHtml(cw.code) + ' · ' + escapeHtml(cw.name || '') + '</button>';
|
|
5273
|
+
}
|
|
5274
|
+
sel += '</div>';
|
|
5275
|
+
}
|
|
5276
|
+
var out = '<div data-pz-wfl-root>' + sel;
|
|
5277
|
+
for (var wi = 0; wi < v.workflows.length; wi++) {
|
|
5278
|
+
var wf = v.workflows[wi];
|
|
5279
|
+
var wpct = wf.total > 0 ? Math.round((wf.done / wf.total) * 100) : 0;
|
|
5280
|
+
out += '<div class="pz-wfl-card" data-pz-wfl-card data-wfl-idx="' + wi + '"'
|
|
5281
|
+
+ (multi && wi !== 0 ? ' style="display:none"' : '') + '>'
|
|
5282
|
+
+ '<div class="pz-wfl-head">'
|
|
5283
|
+
+ '<div class="pz-wfl-title">' + escapeHtml(wf.code + ' -- ' + (wf.name || '')) + '</div>'
|
|
5284
|
+
+ '<div class="pz-wfl-kind">' + escapeHtml(kindLbl[wf.kind] || wf.kind || '') + (wf.status === 'closed' ? ' (cerrado)' : '') + '</div>'
|
|
5285
|
+
+ '<div class="pz-wfl-overall">' + (wf.done || 0) + '/' + (wf.total || 0) + ' tareas · ' + wpct + '%</div>'
|
|
5286
|
+
+ '<div class="pz-wfl-bar"><div class="pz-wfl-fill" style="width:' + wpct + '%"></div></div>'
|
|
5287
|
+
+ '</div>';
|
|
5288
|
+
out += '<div class="pz-wfl-flow">';
|
|
5289
|
+
for (var si2 = 0; si2 < wf.stages.length; si2++) {
|
|
5290
|
+
var st2 = wf.stages[si2];
|
|
5291
|
+
var tasks2 = st2.tasks || [];
|
|
5292
|
+
var sdone = 0; for (var ti = 0; ti < tasks2.length; ti++) if (tasks2[ti].closed) sdone++;
|
|
5293
|
+
var sst = tasks2.length === 0 ? 'pending' : (sdone === tasks2.length ? 'done' : (sdone > 0 ? 'active' : 'pending'));
|
|
5294
|
+
var last2 = si2 === wf.stages.length - 1;
|
|
5295
|
+
out += '<div class="pz-wfl-col">'
|
|
5296
|
+
+ '<div class="pz-wfl-phase wfl-' + sst + (last2 ? ' wfl-last' : '') + '">'
|
|
5297
|
+
+ '<div class="pz-wfl-phase-title">' + escapeHtml(st2.title || st2.id || '') + '</div>'
|
|
5298
|
+
+ '<div class="pz-wfl-phase-count">' + sdone + '/' + tasks2.length + '</div>'
|
|
5299
|
+
+ '</div><div class="pz-wfl-hang">';
|
|
5300
|
+
for (var k2 = 0; k2 < tasks2.length; k2++) {
|
|
5301
|
+
var tk = tasks2[k2];
|
|
5302
|
+
out += '<div class="pz-wfl-task wfl-' + (tk.closed ? 'done' : 'pending') + '" title="' + escapeHtml(tk.id) + '">'
|
|
5303
|
+
+ '<span class="pz-wfl-check">' + ic2(tk.closed) + '</span>'
|
|
5304
|
+
+ '<span class="pz-wfl-task-title">' + escapeHtml((tk.id ? tk.id + ' ' : '') + (tk.title || '')) + (tk.ad_hoc ? ' <span class="pz-wfl-adhoc">+</span>' : '') + '</span>'
|
|
5305
|
+
+ '</div>';
|
|
5306
|
+
}
|
|
5307
|
+
if (tasks2.length === 0) out += '<div class="pz-wfl-task wfl-pending pz-wfl-empty-stage">sin tareas</div>';
|
|
5308
|
+
out += '</div></div>';
|
|
5309
|
+
}
|
|
5310
|
+
out += '</div></div>';
|
|
5311
|
+
}
|
|
5312
|
+
out += '</div>';
|
|
5313
|
+
return out;
|
|
5314
|
+
}
|
|
4845
5315
|
if (!v || !Array.isArray(v.stages)) {
|
|
4846
5316
|
return '<div class="pz-wfl-empty">Sin estado de workflow todavia.</div>';
|
|
4847
5317
|
}
|
|
@@ -4882,6 +5352,161 @@ function renderWorkflowView(content) {
|
|
|
4882
5352
|
return h;
|
|
4883
5353
|
}
|
|
4884
5354
|
|
|
5355
|
+
/* Renderer BPMN 2.0 (pestana canonica de flujos). El content es un
|
|
5356
|
+
* modelo JSON { lanes?:[{id,label}], nodes:[{id,type,label,lane?,
|
|
5357
|
+
* gateway?}], flows:[{from,to,label?}] }. Layout en capas izq->der
|
|
5358
|
+
* (longest-path), shapes BPMN (eventos=circulos, tareas=rect
|
|
5359
|
+
* redondeado, gateways=rombo con simbolo), flujos ortogonales con
|
|
5360
|
+
* flecha + label, swimlanes opcionales. Estatico, estetica sumi-e. */
|
|
5361
|
+
function bpmnWrap(s, maxChars, maxLines) {
|
|
5362
|
+
var words = String(s == null ? '' : s).split(/\s+/);
|
|
5363
|
+
var lines = []; var cur = '';
|
|
5364
|
+
for (var i = 0; i < words.length; i++) {
|
|
5365
|
+
var w = words[i];
|
|
5366
|
+
if (!cur) { cur = w; }
|
|
5367
|
+
else if ((cur + ' ' + w).length <= maxChars) { cur += ' ' + w; }
|
|
5368
|
+
else { lines.push(cur); cur = w; if (lines.length === maxLines - 1) break; }
|
|
5369
|
+
}
|
|
5370
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
5371
|
+
if (lines.length === maxLines && (cur || words.length > 0)) {
|
|
5372
|
+
var last = lines[maxLines - 1];
|
|
5373
|
+
if (last && last.length > maxChars) lines[maxLines - 1] = last.slice(0, maxChars - 1) + '…';
|
|
5374
|
+
}
|
|
5375
|
+
return lines;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
function renderBpmnView(content) {
|
|
5379
|
+
var m;
|
|
5380
|
+
try { m = JSON.parse(content); } catch (e) {
|
|
5381
|
+
return '<div class="pz-bpmn-empty">El flujo BPMN espera JSON { nodes, flows, lanes? }. No pude parsear el content.</div>';
|
|
5382
|
+
}
|
|
5383
|
+
if (!m || !Array.isArray(m.nodes) || m.nodes.length === 0) {
|
|
5384
|
+
return '<div class="pz-bpmn-empty">Sin nodos en el flujo.</div>';
|
|
5385
|
+
}
|
|
5386
|
+
var nodes = m.nodes.filter(function (n) { return n && n.id; });
|
|
5387
|
+
var flows = Array.isArray(m.flows) ? m.flows.filter(function (f) { return f && f.from && f.to; }) : [];
|
|
5388
|
+
var lanes = Array.isArray(m.lanes) ? m.lanes.filter(function (l) { return l && l.id; }) : [];
|
|
5389
|
+
var byId = {}; nodes.forEach(function (n) { byId[n.id] = n; });
|
|
5390
|
+
/* capas: longest-path desde las fuentes (DAG; cap por #nodos si hay ciclos). */
|
|
5391
|
+
var layer = {}; nodes.forEach(function (n) { layer[n.id] = 0; });
|
|
5392
|
+
for (var it = 0; it < nodes.length; it++) {
|
|
5393
|
+
var changed = false;
|
|
5394
|
+
for (var k = 0; k < flows.length; k++) {
|
|
5395
|
+
var f = flows[k];
|
|
5396
|
+
if (byId[f.from] && byId[f.to] && layer[f.to] < layer[f.from] + 1) { layer[f.to] = layer[f.from] + 1; changed = true; }
|
|
5397
|
+
}
|
|
5398
|
+
if (!changed) break;
|
|
5399
|
+
}
|
|
5400
|
+
var maxCol = 0; nodes.forEach(function (n) { if (layer[n.id] > maxCol) maxCol = layer[n.id]; });
|
|
5401
|
+
var hasLanes = lanes.length > 0;
|
|
5402
|
+
var laneIdx = {}; lanes.forEach(function (l, i) { laneIdx[l.id] = i; });
|
|
5403
|
+
/* dimensiones */
|
|
5404
|
+
var COL_W = 178, LANE_H = 116, ROW_H = 96, PAD = 30, LANE_LBL = hasLanes ? 30 : 0, TOPPAD = hasLanes ? 8 : 26;
|
|
5405
|
+
/* posiciones (centro de cada nodo) */
|
|
5406
|
+
var pos = {};
|
|
5407
|
+
if (hasLanes) {
|
|
5408
|
+
var subCount = {}; /* nodos por (lane,col) para apilar */
|
|
5409
|
+
nodes.forEach(function (n) {
|
|
5410
|
+
var li = (n.lane && laneIdx[n.lane] != null) ? laneIdx[n.lane] : 0;
|
|
5411
|
+
var key = li + ':' + layer[n.id];
|
|
5412
|
+
var sub = subCount[key] || 0; subCount[key] = sub + 1;
|
|
5413
|
+
pos[n.id] = { li: li, col: layer[n.id], sub: sub };
|
|
5414
|
+
});
|
|
5415
|
+
nodes.forEach(function (n) {
|
|
5416
|
+
var p = pos[n.id]; var key = p.li + ':' + p.col; var cnt = subCount[key];
|
|
5417
|
+
var bandTop = TOPPAD + p.li * LANE_H;
|
|
5418
|
+
n._x = PAD + LANE_LBL + p.col * COL_W + COL_W / 2;
|
|
5419
|
+
n._y = bandTop + (p.sub + 1) * (LANE_H / (cnt + 1));
|
|
5420
|
+
});
|
|
5421
|
+
} else {
|
|
5422
|
+
var cols = {}; nodes.forEach(function (n) { (cols[layer[n.id]] = cols[layer[n.id]] || []).push(n); });
|
|
5423
|
+
var maxRows = 0; for (var c in cols) if (cols[c].length > maxRows) maxRows = cols[c].length;
|
|
5424
|
+
var diagramH = maxRows * ROW_H;
|
|
5425
|
+
for (var cc = 0; cc <= maxCol; cc++) {
|
|
5426
|
+
var arr = cols[cc] || [];
|
|
5427
|
+
var startY = TOPPAD + (diagramH - arr.length * ROW_H) / 2;
|
|
5428
|
+
arr.forEach(function (n, i) {
|
|
5429
|
+
n._x = PAD + cc * COL_W + COL_W / 2;
|
|
5430
|
+
n._y = startY + i * ROW_H + ROW_H / 2;
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
var W = PAD + LANE_LBL + (maxCol + 1) * COL_W + PAD;
|
|
5435
|
+
var H = hasLanes ? (TOPPAD + lanes.length * LANE_H + PAD) : (TOPPAD + (function () { var mx = 0; nodes.forEach(function (n) { if (n._y > mx) mx = n._y; }); return mx + ROW_H / 2; })());
|
|
5436
|
+
var P = { ink: '#1a1a1a', indigo: '#4f5b87', amber: '#c8a04d', tea: '#6f8b5e', vermilion: '#c8472e', line: '#b8ad95', washi: '#f5f0e6', bg2: '#ece6d8' };
|
|
5437
|
+
/* Auto-resize fix (Pablo 2026-06-16): a wide-flat flow (W >> H) used to
|
|
5438
|
+
* be scaled DOWN to the tab width (width:100%), shrinking the nodes to an
|
|
5439
|
+
* unreadable band with dead space below. min-width = the intrinsic width
|
|
5440
|
+
* keeps nodes at their designed size: a narrow flow still fills the width
|
|
5441
|
+
* (scaled up), a wide one renders 1:1 and the container scrolls. */
|
|
5442
|
+
var svg = '<svg class="pz-bpmn-svg" viewBox="0 0 ' + Math.round(W) + ' ' + Math.round(H) + '"'
|
|
5443
|
+
+ ' style="min-width:' + Math.round(W) + 'px"'
|
|
5444
|
+
+ ' preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg">';
|
|
5445
|
+
svg += '<defs><marker id="bpmn-arrow" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">'
|
|
5446
|
+
+ '<path d="M0,0 L7,3 L0,6 z" fill="' + P.ink + '"/></marker></defs>';
|
|
5447
|
+
/* swimlanes */
|
|
5448
|
+
if (hasLanes) {
|
|
5449
|
+
for (var li2 = 0; li2 < lanes.length; li2++) {
|
|
5450
|
+
var ly = TOPPAD + li2 * LANE_H;
|
|
5451
|
+
svg += '<rect x="' + PAD + '" y="' + ly + '" width="' + (W - 2 * PAD) + '" height="' + LANE_H + '" fill="' + (li2 % 2 ? 'rgba(0,0,0,0.025)' : 'transparent') + '" stroke="' + P.line + '" stroke-width="1"/>';
|
|
5452
|
+
svg += '<text x="' + (PAD + 14) + '" y="' + (ly + LANE_H / 2) + '" transform="rotate(-90 ' + (PAD + 14) + ' ' + (ly + LANE_H / 2) + ')" text-anchor="middle" font-size="11" fill="' + P.indigo + '" font-weight="600">' + escapeHtml(String(lanes[li2].label || lanes[li2].id)) + '</text>';
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
/* flujos (debajo de los nodos) */
|
|
5456
|
+
flows.forEach(function (f) {
|
|
5457
|
+
var a = byId[f.from], b = byId[f.to]; if (!a || !b) return;
|
|
5458
|
+
var ar = bpmnHalf(a), br = bpmnHalf(b);
|
|
5459
|
+
var sx, sy = a._y, tx, ty = b._y;
|
|
5460
|
+
var forward = (b._x >= a._x);
|
|
5461
|
+
sx = a._x + (forward ? ar : -ar); tx = b._x - (forward ? br : -br);
|
|
5462
|
+
var midx = (sx + tx) / 2;
|
|
5463
|
+
var d;
|
|
5464
|
+
if (Math.abs(sy - ty) < 2) { d = 'M' + sx + ',' + sy + ' L' + tx + ',' + ty; }
|
|
5465
|
+
else if (forward) { d = 'M' + sx + ',' + sy + ' L' + midx + ',' + sy + ' L' + midx + ',' + ty + ' L' + tx + ',' + ty; }
|
|
5466
|
+
else { /* loop hacia atras: por abajo */ var down = Math.max(a._y, b._y) + 46; d = 'M' + (a._x) + ',' + (a._y + ar) + ' L' + a._x + ',' + down + ' L' + b._x + ',' + down + ' L' + b._x + ',' + (b._y + br); }
|
|
5467
|
+
svg += '<path d="' + d + '" fill="none" stroke="' + P.ink + '" stroke-width="1.4" marker-end="url(#bpmn-arrow)" opacity="0.8"/>';
|
|
5468
|
+
if (f.label) {
|
|
5469
|
+
var lx = forward ? midx : ((a._x + b._x) / 2), lyy = forward ? (sy + ty) / 2 - 4 : (Math.max(a._y, b._y) + 42);
|
|
5470
|
+
svg += '<rect x="' + (lx - String(f.label).length * 3.1 - 3) + '" y="' + (lyy - 9) + '" width="' + (String(f.label).length * 6.2 + 6) + '" height="13" fill="' + P.washi + '" opacity="0.9"/>';
|
|
5471
|
+
svg += '<text x="' + lx + '" y="' + (lyy + 1) + '" text-anchor="middle" font-size="10" fill="' + P.ink + '">' + escapeHtml(String(f.label)) + '</text>';
|
|
5472
|
+
}
|
|
5473
|
+
});
|
|
5474
|
+
/* nodos */
|
|
5475
|
+
nodes.forEach(function (n) {
|
|
5476
|
+
var x = n._x, y = n._y, type = String(n.type || 'task');
|
|
5477
|
+
if (/start|^event$|end|intermediate/.test(type)) {
|
|
5478
|
+
var r = 18, sw = type.indexOf('end') >= 0 ? 3.4 : 1.6;
|
|
5479
|
+
var stroke = type.indexOf('start') >= 0 ? P.tea : (type.indexOf('end') >= 0 ? P.vermilion : P.indigo);
|
|
5480
|
+
svg += '<circle cx="' + x + '" cy="' + y + '" r="' + r + '" fill="' + P.washi + '" stroke="' + stroke + '" stroke-width="' + sw + '"/>';
|
|
5481
|
+
if (type.indexOf('intermediate') >= 0) svg += '<circle cx="' + x + '" cy="' + y + '" r="' + (r - 3.5) + '" fill="none" stroke="' + stroke + '" stroke-width="1.2"/>';
|
|
5482
|
+
if (n.label) svg += '<text x="' + x + '" y="' + (y + r + 13) + '" text-anchor="middle" font-size="10.5" fill="' + P.ink + '">' + escapeHtml(String(n.label).slice(0, 22)) + '</text>';
|
|
5483
|
+
} else if (/gateway|decision|fork|join|merge/.test(type)) {
|
|
5484
|
+
var g = 23, gk = String(n.gateway || (/parallel|fork|join/.test(type) ? 'parallel' : 'exclusive'));
|
|
5485
|
+
svg += '<path d="M' + x + ',' + (y - g) + ' L' + (x + g) + ',' + y + ' L' + x + ',' + (y + g) + ' L' + (x - g) + ',' + y + ' z" fill="' + P.washi + '" stroke="' + P.amber + '" stroke-width="1.8"/>';
|
|
5486
|
+
var sym = gk.charAt(0) === 'p' ? '+' : (gk.charAt(0) === 'i' ? 'O' : '\\u00d7');
|
|
5487
|
+
svg += '<text x="' + x + '" y="' + (y + 5) + '" text-anchor="middle" font-size="16" fill="' + P.amber + '" font-weight="700">' + sym + '</text>';
|
|
5488
|
+
if (n.label) svg += '<text x="' + x + '" y="' + (y + g + 13) + '" text-anchor="middle" font-size="10.5" fill="' + P.ink + '">' + escapeHtml(String(n.label).slice(0, 22)) + '</text>';
|
|
5489
|
+
} else {
|
|
5490
|
+
var tw = 118, th = 54;
|
|
5491
|
+
svg += '<rect x="' + (x - tw / 2) + '" y="' + (y - th / 2) + '" width="' + tw + '" height="' + th + '" rx="9" fill="' + P.bg2 + '" stroke="' + P.indigo + '" stroke-width="1.4"/>';
|
|
5492
|
+
var ic = { userTask: '\\u263a', serviceTask: '\\u2699', scriptTask: '\\u25b8', manualTask: '\\u270b', sendTask: '\\u2709', receiveTask: '\\u2709' }[type];
|
|
5493
|
+
if (ic) svg += '<text x="' + (x - tw / 2 + 9) + '" y="' + (y - th / 2 + 14) + '" font-size="11" fill="' + P.indigo + '">' + ic + '</text>';
|
|
5494
|
+
var ls = bpmnWrap(n.label || n.id, 17, 3);
|
|
5495
|
+
var ty0 = y - (ls.length - 1) * 7;
|
|
5496
|
+
for (var li3 = 0; li3 < ls.length; li3++) svg += '<text x="' + x + '" y="' + (ty0 + li3 * 14 + 4) + '" text-anchor="middle" font-size="11.5" fill="' + P.ink + '">' + escapeHtml(ls[li3]) + '</text>';
|
|
5497
|
+
}
|
|
5498
|
+
});
|
|
5499
|
+
svg += '</svg>';
|
|
5500
|
+
return svg;
|
|
5501
|
+
}
|
|
5502
|
+
/* mitad del ancho/alto del nodo para anclar los flujos al borde. */
|
|
5503
|
+
function bpmnHalf(n) {
|
|
5504
|
+
var type = String(n.type || 'task');
|
|
5505
|
+
if (/start|^event$|end|intermediate/.test(type)) return 18;
|
|
5506
|
+
if (/gateway|decision|fork|join|merge/.test(type)) return 23;
|
|
5507
|
+
return 59;
|
|
5508
|
+
}
|
|
5509
|
+
|
|
4885
5510
|
const CODING_TAB_ID = 'coding_canvas';
|
|
4886
5511
|
const yfCoding = {
|
|
4887
5512
|
strokes: [], texts: [], fade: 0, phase: 'draw', /* draw | fadeout */
|
|
@@ -4934,8 +5559,12 @@ function renderCodingDetail() {
|
|
|
4934
5559
|
var item = feed[i];
|
|
4935
5560
|
var d = item.detail || { kind: 'other', target: item.text, path: null, diff: null, body: null };
|
|
4936
5561
|
var icon = CODING_ICON[d.kind] || '·';
|
|
5562
|
+
var ts = '';
|
|
5563
|
+
try { ts = new Date(item.t).toTimeString().slice(0, 8); } catch (e) { ts = ''; }
|
|
4937
5564
|
h += '<div class="pz-cw-row k-' + escapeHtml(d.kind || 'other') + '">'
|
|
4938
|
-
+ '<div class="pz-cw-head"
|
|
5565
|
+
+ '<div class="pz-cw-head">'
|
|
5566
|
+
+ '<span class="pz-cw-ts">' + ts + '</span>'
|
|
5567
|
+
+ '<span class="pz-cw-ic">' + icon + '</span>'
|
|
4939
5568
|
+ '<span class="pz-cw-target">' + escapeHtml(d.target || item.text || '') + '</span>'
|
|
4940
5569
|
+ (d.path ? '<span class="pz-cw-path">' + escapeHtml(d.path) + '</span>' : '')
|
|
4941
5570
|
+ '</div>';
|
|
@@ -5276,10 +5905,11 @@ function unmarkStartupTabClosed(key) {
|
|
|
5276
5905
|
* titulo real lo recordamos del ultimo fetch; el fallback cubre las que
|
|
5277
5906
|
* nunca se vieron. */
|
|
5278
5907
|
var yfStartupTitles = {};
|
|
5279
|
-
var NATIVE_TAB_KEYS = ['startup_graph', 'startup_graph_sem', 'startup_pendings', 'startup_workflow', 'coding_canvas'];
|
|
5908
|
+
var NATIVE_TAB_KEYS = ['startup_graph', 'startup_graph_sem', 'startup_pendings', 'startup_workflow', 'startup_tasks', 'coding_canvas'];
|
|
5280
5909
|
var NATIVE_TAB_FALLBACK = {
|
|
5281
5910
|
startup_graph: 'Grafo', startup_graph_sem: 'Grafo semantico',
|
|
5282
|
-
startup_pendings: 'Pendientes', startup_workflow: 'Workflow',
|
|
5911
|
+
startup_pendings: 'Pendientes', startup_workflow: 'Workflow',
|
|
5912
|
+
startup_tasks: 'Tareas', coding_canvas: 'Coding',
|
|
5283
5913
|
};
|
|
5284
5914
|
function nativeTabLabel(key) { return yfStartupTitles[key] || NATIVE_TAB_FALLBACK[key] || key; }
|
|
5285
5915
|
|
|
@@ -5305,8 +5935,15 @@ function pizarronUpsertById(item) {
|
|
|
5305
5935
|
const wasEditing = existing.isEditing;
|
|
5306
5936
|
/* No pisar si el usuario esta editando esa tab. */
|
|
5307
5937
|
if (!wasEditing) {
|
|
5938
|
+
/* Solo re-renderizar si el contenido/tipo/titulo CAMBIO. Antes se
|
|
5939
|
+
* re-renderizaba en cada refresh (cada 12s), lo que reiniciaba el
|
|
5940
|
+
* grafo: reseteaba los filtros de grupo + reposicionaba el layout
|
|
5941
|
+
* aunque nada hubiera cambiado. */
|
|
5942
|
+
const changed = existing.content !== item.content
|
|
5943
|
+
|| existing.kind !== item.kind
|
|
5944
|
+
|| existing.title !== item.title;
|
|
5308
5945
|
Object.assign(existing, item, { id: existing.id, isEditing: false });
|
|
5309
|
-
if (wasActive) renderPizarron();
|
|
5946
|
+
if (wasActive && changed) renderPizarron();
|
|
5310
5947
|
}
|
|
5311
5948
|
return existing.id;
|
|
5312
5949
|
}
|
|
@@ -5391,6 +6028,33 @@ function graphNodeColor(n) {
|
|
|
5391
6028
|
return '#8a7bb0'; /* violeta tenue */
|
|
5392
6029
|
}
|
|
5393
6030
|
|
|
6031
|
+
/* High-level group of a node, for the filter legend -- SAME meaning as
|
|
6032
|
+
* the server's nodeGroup (board_snapshot.ts) so desktop + mobile agree.
|
|
6033
|
+
* indexOf-only (NO regex: this lives inside the panel template literal,
|
|
6034
|
+
* where a regex backslash would be mangled). */
|
|
6035
|
+
const PZ_GROUP_COLORS = {
|
|
6036
|
+
backend: '#5b6aa6', frontend: '#6f8b5e', mobile: '#c2914a', tests: '#b06a6a',
|
|
6037
|
+
scripts: '#8a7bb0', libs: '#9a9a9a', tools: '#4a8a8a', protocol: '#b0894a',
|
|
6038
|
+
};
|
|
6039
|
+
/* Filtro de grupos por tab de grafo (id -> {group:boolean}). Se conserva
|
|
6040
|
+
* entre re-renders para que un refresh con datos nuevos NO reinicie la
|
|
6041
|
+
* seleccion del usuario. */
|
|
6042
|
+
const _graphFilterByTab = {};
|
|
6043
|
+
function graphNodeGroup(n) {
|
|
6044
|
+
const id = String((n && n.id) || '');
|
|
6045
|
+
const s = (id + ' ' + String((n && (n.label || n.name)) || '')).toLowerCase();
|
|
6046
|
+
if (id.indexOf('tool.ci.') === 0) return 'tools';
|
|
6047
|
+
if (id.indexOf('tool.npm.') === 0 || id.indexOf('tool.script.') === 0) return 'scripts';
|
|
6048
|
+
if ((n && n.kind === 'library') || id.indexOf('lib.') === 0) return 'libs';
|
|
6049
|
+
const has = (w) => s.indexOf(w) >= 0;
|
|
6050
|
+
if (has('test') || has('spec') || has('vitest') || has('playwright') || has('.qa') || has('qa.')) return 'tests';
|
|
6051
|
+
if (has('mobile') || has('forge-rn') || has('nac3-rn') || has('expo') || has('metro')) return 'mobile';
|
|
6052
|
+
if (has('panel') || has('pizarron') || has('chat') || has('reader') || has('design')
|
|
6053
|
+
|| has('figma') || has('webview') || has('component') || has('frontend')) return 'frontend';
|
|
6054
|
+
if (n && n.kind === 'protocol') return 'protocol';
|
|
6055
|
+
return 'backend';
|
|
6056
|
+
}
|
|
6057
|
+
|
|
5394
6058
|
function initGraphTab(it) {
|
|
5395
6059
|
const canvas = document.querySelector('[data-pz-graph-canvas]');
|
|
5396
6060
|
const detail = document.querySelector('[data-pz-graph-detail]');
|
|
@@ -5414,8 +6078,10 @@ function initGraphTab(it) {
|
|
|
5414
6078
|
label: String(n.label != null ? n.label : n.id != null ? n.id : i),
|
|
5415
6079
|
weight: typeof n.weight === 'number' ? n.weight : 0.5,
|
|
5416
6080
|
kind: n.kind || '',
|
|
6081
|
+
group: graphNodeGroup(n),
|
|
5417
6082
|
detail: n.detail || '',
|
|
5418
|
-
|
|
6083
|
+
confidence: n.confidence || 'auto',
|
|
6084
|
+
color: PZ_GROUP_COLORS[graphNodeGroup(n)] || graphNodeColor(n),
|
|
5419
6085
|
x: W / 2 + Math.cos(ang) * Math.min(W, H) * 0.32,
|
|
5420
6086
|
y: H / 2 + Math.sin(ang) * Math.min(W, H) * 0.32,
|
|
5421
6087
|
vx: 0, vy: 0, fixed: false,
|
|
@@ -5470,6 +6136,15 @@ function initGraphTab(it) {
|
|
|
5470
6136
|
const circle = document.createElementNS(SVGNS, 'circle');
|
|
5471
6137
|
circle.setAttribute('r', String(r));
|
|
5472
6138
|
circle.setAttribute('fill', n.color);
|
|
6139
|
+
/* PND graph -- nodos 'claimed' (declarados en chat, sin verificacion
|
|
6140
|
+
* automatica) se dibujan atenuados + con borde punteado para
|
|
6141
|
+
* distinguirlos de lo derivado del codigo (auto). Antes se ocultaban. */
|
|
6142
|
+
if (n.confidence === 'claimed') {
|
|
6143
|
+
circle.setAttribute('fill-opacity', '0.45');
|
|
6144
|
+
circle.setAttribute('stroke', n.color);
|
|
6145
|
+
circle.setAttribute('stroke-width', '1.5');
|
|
6146
|
+
circle.setAttribute('stroke-dasharray', '3 2');
|
|
6147
|
+
}
|
|
5473
6148
|
g.appendChild(circle);
|
|
5474
6149
|
const label = document.createElementNS(SVGNS, 'text');
|
|
5475
6150
|
label.setAttribute('class', 'pz-node-label');
|
|
@@ -5482,6 +6157,48 @@ function initGraphTab(it) {
|
|
|
5482
6157
|
return g;
|
|
5483
6158
|
});
|
|
5484
6159
|
|
|
6160
|
+
/* ---- Filtro por grupo (legend toggleable) -- desktop a la par del
|
|
6161
|
+
* mobile. Colorea por backend/frontend/mobile/tests/tools/scripts/
|
|
6162
|
+
* libs/protocol y permite mostrar/ocultar cada grupo. ---- */
|
|
6163
|
+
/* Restaurar la seleccion previa de este tab (si existe) para que un
|
|
6164
|
+
* refresh no la pierda; grupos nuevos arrancan visibles. */
|
|
6165
|
+
const savedVis = _graphFilterByTab[it.id] || {};
|
|
6166
|
+
const vis = {};
|
|
6167
|
+
const counts = {};
|
|
6168
|
+
nodes.forEach((n) => {
|
|
6169
|
+
vis[n.group] = Object.prototype.hasOwnProperty.call(savedVis, n.group) ? !!savedVis[n.group] : true;
|
|
6170
|
+
counts[n.group] = (counts[n.group] || 0) + 1;
|
|
6171
|
+
});
|
|
6172
|
+
_graphFilterByTab[it.id] = vis; /* el toggle muta este mismo objeto */
|
|
6173
|
+
const legend = document.createElement('div');
|
|
6174
|
+
legend.setAttribute('style', 'position:absolute;top:6px;left:6px;right:6px;display:flex;flex-wrap:wrap;gap:5px;z-index:6;pointer-events:auto;');
|
|
6175
|
+
Object.keys(counts).sort().forEach((k) => {
|
|
6176
|
+
const chip = document.createElement('button');
|
|
6177
|
+
chip.type = 'button';
|
|
6178
|
+
chip.setAttribute('data-g', k);
|
|
6179
|
+
chip.setAttribute('style', 'display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:11px;color:#3a3a3a;'
|
|
6180
|
+
+ 'border:1px solid #d9d2c4;border-radius:14px;padding:3px 9px;background:#fff;cursor:pointer;opacity:' + (vis[k] ? '1' : '0.4') + ';');
|
|
6181
|
+
chip.innerHTML = '<span style="width:9px;height:9px;border-radius:50%;background:' + (PZ_GROUP_COLORS[k] || '#999') + '"></span>'
|
|
6182
|
+
+ escapeHtml(k) + ' ' + counts[k];
|
|
6183
|
+
chip.addEventListener('click', () => {
|
|
6184
|
+
vis[k] = !vis[k];
|
|
6185
|
+
chip.style.opacity = vis[k] ? '1' : '0.4';
|
|
6186
|
+
applyGraphFilter();
|
|
6187
|
+
});
|
|
6188
|
+
legend.appendChild(chip);
|
|
6189
|
+
});
|
|
6190
|
+
/* el canvas es position:relative en CSS; si no, el absolute cuelga del svg */
|
|
6191
|
+
canvas.style.position = canvas.style.position || 'relative';
|
|
6192
|
+
canvas.appendChild(legend);
|
|
6193
|
+
function applyGraphFilter() {
|
|
6194
|
+
for (let i = 0; i < nodeEls.length; i++) nodeEls[i].style.display = vis[nodes[i].group] ? '' : 'none';
|
|
6195
|
+
for (let k = 0; k < edges.length; k++) {
|
|
6196
|
+
const show = vis[nodes[edges[k].from].group] && vis[nodes[edges[k].to].group];
|
|
6197
|
+
edgeEls[k].style.display = show ? '' : 'none';
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
applyGraphFilter();
|
|
6201
|
+
|
|
5485
6202
|
function showDetail(n) {
|
|
5486
6203
|
if (!detail) return;
|
|
5487
6204
|
nodeEls.forEach((g) => g.classList.toggle('selected', g._node === n));
|
|
@@ -5681,13 +6398,67 @@ function setStatus(text, isErr) {
|
|
|
5681
6398
|
|
|
5682
6399
|
function setBusy(b) {
|
|
5683
6400
|
state.busy = b;
|
|
5684
|
-
|
|
6401
|
+
/* Queue (task #28) -- the input + Send stay LIVE during a turn so the
|
|
6402
|
+
* user can type-while-working (like Claude Code): a send mid-turn is
|
|
6403
|
+
* queued and runs automatically when the turn finishes. Only the clip
|
|
6404
|
+
* (attachments mid-turn) is gated. */
|
|
6405
|
+
for (const sel of ['#bar-mini .clip', '#bar-full .clip']) {
|
|
6406
|
+
const el = $(sel); if (el) el.disabled = b;
|
|
6407
|
+
}
|
|
6408
|
+
for (const sel of ['#bar-mini input', '#bar-full input', '#bar-mini .send', '#bar-full .send']) {
|
|
6409
|
+
const el = $(sel); if (el) el.disabled = false;
|
|
6410
|
+
}
|
|
6411
|
+
/* Interrupt (task #27) -- a Detener button appears alongside Send
|
|
6412
|
+
* while a turn is in flight, so the user can stop a long / runaway
|
|
6413
|
+
* turn (a controlled fall: applied tool calls stay, the partial
|
|
6414
|
+
* answer is kept). */
|
|
6415
|
+
for (const sel of ['#bar-mini .stop', '#bar-full .stop']) {
|
|
5685
6416
|
const el = $(sel);
|
|
5686
|
-
if (el) el.
|
|
6417
|
+
if (el) { el.style.display = b ? '' : 'none'; el.disabled = false; }
|
|
5687
6418
|
}
|
|
6419
|
+
renderQueue();
|
|
5688
6420
|
renderStream();
|
|
5689
6421
|
}
|
|
5690
6422
|
|
|
6423
|
+
/* Queue (task #28) -- render the pending-message chips above the
|
|
6424
|
+
* composer. Each chip is removable before it runs. */
|
|
6425
|
+
function renderQueue() {
|
|
6426
|
+
const q = state.queue || [];
|
|
6427
|
+
for (const id of ['#queue-mini', '#queue-full']) {
|
|
6428
|
+
const el = $(id);
|
|
6429
|
+
if (!el) continue;
|
|
6430
|
+
if (!q.length) { el.style.display = 'none'; el.innerHTML = ''; continue; }
|
|
6431
|
+
el.style.display = '';
|
|
6432
|
+
el.innerHTML = '<span class="queue-label">' + escapeHtml(CONFIG.i18n.queueLabel) + '</span>'
|
|
6433
|
+
+ q.map(function (txt, i) {
|
|
6434
|
+
const short = txt.length > 42 ? txt.slice(0, 42) + '...' : txt;
|
|
6435
|
+
return '<span class="queue-chip" data-qrm="' + i + '" role="button" tabindex="0"'
|
|
6436
|
+
+ ' title="' + escapeHtml(CONFIG.i18n.queueRemove) + ': ' + escapeHtml(txt) + '">'
|
|
6437
|
+
+ escapeHtml(short) + ' <span class="queue-x">×</span></span>';
|
|
6438
|
+
}).join('');
|
|
6439
|
+
el.querySelectorAll('[data-qrm]').forEach(function (chip) {
|
|
6440
|
+
chip.addEventListener('click', function () {
|
|
6441
|
+
const idx = parseInt(chip.getAttribute('data-qrm'), 10);
|
|
6442
|
+
if (!isNaN(idx) && state.queue) { state.queue.splice(idx, 1); renderQueue(); }
|
|
6443
|
+
});
|
|
6444
|
+
});
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
/* Interrupt (task #27) -- POST the stop request. The server aborts the
|
|
6449
|
+
* active turn's signal; the in-flight send() unwinds via the SSE
|
|
6450
|
+
* turn_complete (interrupted:true) or the abort path. */
|
|
6451
|
+
async function interruptTurn() {
|
|
6452
|
+
setStatus(CONFIG.i18n.statusStopping);
|
|
6453
|
+
try {
|
|
6454
|
+
const r = await fetch('/api/chat/interrupt', { method: 'POST' });
|
|
6455
|
+
const j = await r.json().catch(function () { return {}; });
|
|
6456
|
+
if (!j || j.active === false) setStatus(CONFIG.i18n.statusNoActiveTurn);
|
|
6457
|
+
} catch (e) {
|
|
6458
|
+
setStatus(CONFIG.i18n.statusInterruptFailed, true);
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
|
|
5691
6462
|
/* alpha.59e -- consume an SSE response stream from /api/chat.
|
|
5692
6463
|
* The body is text/event-stream with frames separated by an
|
|
5693
6464
|
* empty line. We parse each event as it arrives + update the
|
|
@@ -5878,7 +6649,18 @@ function handleSseEvent(event, data) {
|
|
|
5878
6649
|
|
|
5879
6650
|
async function send(text, opts) {
|
|
5880
6651
|
text = (text || '').trim();
|
|
5881
|
-
if (!text
|
|
6652
|
+
if (!text) return;
|
|
6653
|
+
/* Queue (task #28) -- a turn is already running: enqueue this message
|
|
6654
|
+
* instead of dropping it. It runs automatically when the current turn
|
|
6655
|
+
* finishes (drained in the finally below). Synthetic 'hidden' turns
|
|
6656
|
+
* (NAC3 approval continuations) and the drain itself bypass the queue.
|
|
6657
|
+
* 'Send now' steering = press Detener, then the queued message runs. */
|
|
6658
|
+
if (state.busy && !(opts && (opts.hidden || opts.fromQueue))) {
|
|
6659
|
+
if (!state.queue) state.queue = [];
|
|
6660
|
+
state.queue.push(text);
|
|
6661
|
+
renderQueue();
|
|
6662
|
+
return;
|
|
6663
|
+
}
|
|
5882
6664
|
/* alpha.49 -- opts.hidden=true pushes a user message that the
|
|
5883
6665
|
* LLM sees in context but the UI does NOT render in the bubble
|
|
5884
6666
|
* stream. Used by the NAC3 approval modal post-action to inject
|
|
@@ -5963,7 +6745,12 @@ async function send(text, opts) {
|
|
|
5963
6745
|
'yf limits set max_tool_rounds.',
|
|
5964
6746
|
};
|
|
5965
6747
|
var friendly = FRIENDLY_ERRORS[data.error];
|
|
5966
|
-
|
|
6748
|
+
/* Surface the DETAILED message (stderr del subproceso, motivo real
|
|
6749
|
+
* del fallo) en vez del codigo generico -- p.ej. modo Plan
|
|
6750
|
+
* claude_api_error traia el stderr del subproceso escondido.
|
|
6751
|
+
* (Pablo 2026-06-15) */
|
|
6752
|
+
var detail = (data.message && data.message !== data.error) ? data.message : '';
|
|
6753
|
+
setStatus(friendly || detail || data.error || ('HTTP ' + res.status), true);
|
|
5967
6754
|
// Drop the unanswered user message so the user can retry.
|
|
5968
6755
|
state.messages.pop();
|
|
5969
6756
|
return;
|
|
@@ -5971,6 +6758,13 @@ async function send(text, opts) {
|
|
|
5971
6758
|
/* alpha.58 -- attach brain_routing metadata to the bubble so
|
|
5972
6759
|
* renderStream can paint a tier badge + escalation arrow. */
|
|
5973
6760
|
const assistantMsg = { role: 'assistant', content: data.message.text };
|
|
6761
|
+
/* Interrupt (task #27) -- the turn was stopped mid-flight; mark the
|
|
6762
|
+
* partial answer so the user knows it is not the full reply. */
|
|
6763
|
+
if (data.interrupted === true) {
|
|
6764
|
+
assistantMsg.interrupted = true;
|
|
6765
|
+
assistantMsg.content = (assistantMsg.content || '') + '\\n\\n_' + CONFIG.i18n.labelInterrupted + '_';
|
|
6766
|
+
setStatus(CONFIG.i18n.statusTurnInterrupted);
|
|
6767
|
+
}
|
|
5974
6768
|
if (data.brain_routing && typeof data.brain_routing === 'object') {
|
|
5975
6769
|
const br = data.brain_routing;
|
|
5976
6770
|
assistantMsg.brain_tier = br.tier;
|
|
@@ -6143,11 +6937,23 @@ async function send(text, opts) {
|
|
|
6143
6937
|
* goes back to default state on the next turn. */
|
|
6144
6938
|
state.streamProgress = null;
|
|
6145
6939
|
setBusy(false);
|
|
6940
|
+
/* Queue (task #28) -- drain the next queued message, if any. A small
|
|
6941
|
+
* delay lets the DOM settle (busy flag cleared) before the next turn
|
|
6942
|
+
* starts; fromQueue bypasses the re-enqueue guard. */
|
|
6943
|
+
if (state.queue && state.queue.length) {
|
|
6944
|
+
const next = state.queue.shift();
|
|
6945
|
+
renderQueue();
|
|
6946
|
+
setTimeout(function () { send(next, { fromQueue: true }); }, 60);
|
|
6947
|
+
}
|
|
6146
6948
|
}
|
|
6147
6949
|
}
|
|
6148
6950
|
|
|
6149
6951
|
function attachBar(form) {
|
|
6150
6952
|
const input = form.querySelector('input');
|
|
6953
|
+
/* Interrupt (task #27) -- the Detener button shares the composer
|
|
6954
|
+
* row; wire it to the stop endpoint. */
|
|
6955
|
+
const stopBtn = form.querySelector('[data-stop]');
|
|
6956
|
+
if (stopBtn) stopBtn.addEventListener('click', function () { interruptTurn(); });
|
|
6151
6957
|
form.addEventListener('submit', (e) => {
|
|
6152
6958
|
e.preventDefault();
|
|
6153
6959
|
const v = input.value;
|
|
@@ -6604,15 +7410,7 @@ async function pizarronPlanHintCheck() {
|
|
|
6604
7410
|
try {
|
|
6605
7411
|
const r = await fetch('/api/forge/brain-config');
|
|
6606
7412
|
const data = await r.json();
|
|
6607
|
-
|
|
6608
|
-
if (isCli) {
|
|
6609
|
-
/* El bridge esta activo: arrancar el polling de la cola.
|
|
6610
|
-
* El aviso queda oculto porque el pizarron SI funciona. */
|
|
6611
|
-
if (hint) hint.classList.add('hidden');
|
|
6612
|
-
startPanelQueuePolling();
|
|
6613
|
-
} else {
|
|
6614
|
-
if (hint) hint.classList.add('hidden');
|
|
6615
|
-
}
|
|
7413
|
+
if (hint) hint.classList.add('hidden');
|
|
6616
7414
|
} catch { /* dejar como esta */ }
|
|
6617
7415
|
}
|
|
6618
7416
|
|
|
@@ -6655,9 +7453,26 @@ function materializePanelDispatch(d) {
|
|
|
6655
7453
|
/* proyecto nuevo: re-mostrar las 4 pestanas canonicas (respeta
|
|
6656
7454
|
* el switch en el endpoint). Se limpia el set de cerradas. */
|
|
6657
7455
|
try { localStorage.removeItem(YF_CLOSED_STARTUP_KEY); } catch (e) { /* */ }
|
|
7456
|
+
/* Project selector (task #33) -- update the header label to the new
|
|
7457
|
+
* project name the server reported. */
|
|
7458
|
+
try {
|
|
7459
|
+
if (d.args && d.args.name) { CONFIG.projectName = String(d.args.name); setProj(); }
|
|
7460
|
+
} catch (e) { /* */ }
|
|
6658
7461
|
loadStartupTabs(false).catch(function () {});
|
|
6659
7462
|
return;
|
|
6660
7463
|
}
|
|
7464
|
+
if (d.verb === 'system.remote-turn') {
|
|
7465
|
+
/* UI sync (task #32) -- a turn issued from the phone in the unified
|
|
7466
|
+
* 'notebook' thread ran on the desktop; show it here so the laptop
|
|
7467
|
+
* reflects what was asked + answered from the mobile. */
|
|
7468
|
+
const ra = d.args || {};
|
|
7469
|
+
if (ra.user) state.messages.push({ role: 'user', content: String(ra.user), remote: true });
|
|
7470
|
+
if (typeof ra.reply === 'string' && ra.reply) {
|
|
7471
|
+
state.messages.push({ role: 'assistant', content: ra.reply, remote: true });
|
|
7472
|
+
}
|
|
7473
|
+
renderStream();
|
|
7474
|
+
return;
|
|
7475
|
+
}
|
|
6661
7476
|
if (!d.args) return;
|
|
6662
7477
|
const a = d.args;
|
|
6663
7478
|
if (d.verb === 'pizarron.create-tab') {
|
|
@@ -6697,10 +7512,14 @@ function materializePanelDispatch(d) {
|
|
|
6697
7512
|
/* ---- PND-069: seccion Brain del panel de settings ----------- */
|
|
6698
7513
|
const yfBrain = { cfg: null, loading: false };
|
|
6699
7514
|
|
|
7515
|
+
/* Strict BYOK: the everyday brain is always the user own key
|
|
7516
|
+
* (Console / Plan / other providers). The yujin_managed virtual
|
|
7517
|
+
* provider is intentionally NOT offered here -- managed access is
|
|
7518
|
+
* reserved for the bounded onboarding concierge, never the daily brain. */
|
|
6700
7519
|
const BRAIN_PROVIDER_LABELS = {
|
|
6701
7520
|
anthropic: 'Anthropic', openai: 'OpenAI', google_ai: 'Google AI',
|
|
6702
7521
|
deepseek: 'DeepSeek', xai: 'xAI', mistral: 'Mistral',
|
|
6703
|
-
cohere: 'Cohere',
|
|
7522
|
+
cohere: 'Cohere',
|
|
6704
7523
|
};
|
|
6705
7524
|
|
|
6706
7525
|
function brainStatus(msg) {
|
|
@@ -6805,22 +7624,34 @@ async function brainConfigSave(patch, statusMsg) {
|
|
|
6805
7624
|
body: JSON.stringify(patch),
|
|
6806
7625
|
});
|
|
6807
7626
|
const data = await r.json();
|
|
6808
|
-
if (!r.ok || !data.ok) { brainStatus('error guardando'); return; }
|
|
7627
|
+
if (!r.ok || !data.ok) { brainStatus('error guardando'); return null; }
|
|
6809
7628
|
yfBrain.cfg = data.config;
|
|
6810
7629
|
brainStatus(statusMsg || 'guardado');
|
|
6811
7630
|
setTimeout(() => brainStatus(''), 2500);
|
|
7631
|
+
return data;
|
|
6812
7632
|
} catch (err) {
|
|
6813
7633
|
brainStatus('error: ' + (err && err.message ? err.message : String(err)));
|
|
7634
|
+
return null;
|
|
6814
7635
|
}
|
|
6815
7636
|
}
|
|
6816
7637
|
|
|
6817
7638
|
function brainConfigBind() {
|
|
6818
7639
|
document.querySelectorAll('input[name="brain-anthropic-mode"]').forEach((radio) => {
|
|
6819
|
-
radio.addEventListener('change', () => {
|
|
7640
|
+
radio.addEventListener('change', async () => {
|
|
6820
7641
|
if (!radio.checked) return;
|
|
6821
7642
|
const note = document.querySelector('[data-brain-mode-note]');
|
|
6822
|
-
|
|
6823
|
-
brainConfigSave({ anthropic_mode: radio.value }, '
|
|
7643
|
+
const modeLabel = radio.value === 'plan' ? 'Plan (suscripcion)' : 'Console (API key)';
|
|
7644
|
+
const data = await brainConfigSave({ anthropic_mode: radio.value }, 'aplicando ' + modeLabel + '...');
|
|
7645
|
+
/* Hot-swap (server applies live now). Only show the restart hint if
|
|
7646
|
+
* the swap did NOT apply live (e.g. test client). */
|
|
7647
|
+
const live = data && data.applied_live;
|
|
7648
|
+
if (note) {
|
|
7649
|
+
note.hidden = false;
|
|
7650
|
+
note.textContent = live
|
|
7651
|
+
? modeLabel + ' aplicado. El proximo turno ya usa este modo.'
|
|
7652
|
+
: 'Se aplica al reabrir el panel.';
|
|
7653
|
+
}
|
|
7654
|
+
if (live) brainStatus(modeLabel + ' activo');
|
|
6824
7655
|
});
|
|
6825
7656
|
});
|
|
6826
7657
|
const ecoToggle = document.querySelector('[data-brain-eco-enabled]');
|
|
@@ -8356,6 +9187,11 @@ function installSupportCollector() {
|
|
|
8356
9187
|
document.addEventListener('DOMContentLoaded', () => {
|
|
8357
9188
|
installSupportCollector();
|
|
8358
9189
|
setProj();
|
|
9190
|
+
/* Project selector (task #33) -- the header project label opens the
|
|
9191
|
+
* switcher. */
|
|
9192
|
+
$$('[data-proj-switch]').forEach(function (el) {
|
|
9193
|
+
el.addEventListener('click', function (e) { e.stopPropagation(); openProjectMenu(el); });
|
|
9194
|
+
});
|
|
8359
9195
|
/* alpha.43 F1 -- prime the NAC3 manifest cache on boot so the
|
|
8360
9196
|
first chat-response version stamp has something to compare
|
|
8361
9197
|
against. Silent: if the panel never sent a request before,
|
|
@@ -8574,6 +9410,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
8574
9410
|
brainConfigBind();
|
|
8575
9411
|
panelZoomMaximizeBind();
|
|
8576
9412
|
pizarronPlanHintCheck();
|
|
9413
|
+
/* Panel-queue polling runs in ALL modes (Pablo 2026-06-15): the queue
|
|
9414
|
+
* carries cross-cutting events -- system.remote-turn (mobile chats in
|
|
9415
|
+
* the unified session) + system.reanchor (project switch) + chat-log --
|
|
9416
|
+
* that must reflect in Console/API mode too, not only Plan/CLI. The bug
|
|
9417
|
+
* was gating the poller on claude-code-cli, so in Console mode the
|
|
9418
|
+
* desktop never saw mobile turns nor a project switch. */
|
|
9419
|
+
startPanelQueuePolling();
|
|
8577
9420
|
/* Click outside closes. */
|
|
8578
9421
|
document.addEventListener('click', (e) => {
|
|
8579
9422
|
if (!settingsPanel || settingsPanel.classList.contains('hidden')) return;
|