create-walle 0.9.21 → 0.9.23
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 +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
const CR = {};
|
|
7
7
|
let pinnedProjects = []; // Array of pinned project paths, persisted via savePref
|
|
8
|
+
let projectChooserState = {
|
|
9
|
+
projects: [],
|
|
10
|
+
query: '',
|
|
11
|
+
filter: 'all',
|
|
12
|
+
scan: null,
|
|
13
|
+
};
|
|
14
|
+
let projectChooserRefreshTimer = null;
|
|
8
15
|
|
|
9
16
|
let crState = {
|
|
10
17
|
reviewType: 'code', // code | doc
|
|
@@ -16,8 +23,10 @@ let crState = {
|
|
|
16
23
|
files: [], // Parsed diff files
|
|
17
24
|
activeFile: null, // Currently viewed file path
|
|
18
25
|
comments: [], // All comments for current review
|
|
26
|
+
viewedFiles: new Set(),// Paths marked "Viewed" (review progress), persisted per review+base
|
|
19
27
|
diffMode: 'unified', // unified | split
|
|
20
28
|
document: null, // Current document artifact in doc review mode
|
|
29
|
+
documentCandidates: null,
|
|
21
30
|
docViewMode: 'rendered',// rendered | source | split
|
|
22
31
|
activeBlock: null, // Current document block id
|
|
23
32
|
docLine: 1,
|
|
@@ -40,7 +49,10 @@ async function api(path, method, body) {
|
|
|
40
49
|
let data = null;
|
|
41
50
|
try { data = await res.json(); } catch (_) {}
|
|
42
51
|
if (!res.ok || (data && data.error)) {
|
|
43
|
-
|
|
52
|
+
const err = new Error((data && data.error) || ('HTTP ' + res.status));
|
|
53
|
+
err.status = res.status;
|
|
54
|
+
err.data = data;
|
|
55
|
+
throw err;
|
|
44
56
|
}
|
|
45
57
|
return data || {};
|
|
46
58
|
}
|
|
@@ -54,6 +66,63 @@ function basename(p) {
|
|
|
54
66
|
return String(p || '').split('/').filter(Boolean).pop() || 'document';
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
function replaceHash(hash) {
|
|
70
|
+
history.replaceState(null, '', location.pathname + location.search + (hash || ''));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function paramsFromHash(hash) {
|
|
74
|
+
const raw = String(hash || '').replace(/^#/, '');
|
|
75
|
+
const amp = raw.indexOf('&');
|
|
76
|
+
if (amp >= 0) return new URLSearchParams(raw.slice(amp + 1));
|
|
77
|
+
return new URLSearchParams(raw);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sessionIdFromHash(hash) {
|
|
81
|
+
const raw = String(hash || '').replace(/^#/, '');
|
|
82
|
+
if (raw.startsWith('session=')) return decodeURIComponent(raw.slice('session='.length));
|
|
83
|
+
const params = paramsFromHash(hash);
|
|
84
|
+
return params.get('session') || params.get('sessionId') || '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function activateSessionRoute(sessionId) {
|
|
88
|
+
if (!sessionId) return false;
|
|
89
|
+
const state = window._ctmState || window.state;
|
|
90
|
+
if (state) {
|
|
91
|
+
state.pendingHashSession = sessionId;
|
|
92
|
+
state.pendingHashType = 'active';
|
|
93
|
+
}
|
|
94
|
+
if (state?.sessions && typeof state.sessions.has === 'function' && state.sessions.has(sessionId) &&
|
|
95
|
+
typeof window.activateTab === 'function') {
|
|
96
|
+
window.activateTab(sessionId);
|
|
97
|
+
} else {
|
|
98
|
+
replaceHash('#session=' + encodeURIComponent(sessionId));
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isRecoverableDocumentOpenError(err) {
|
|
104
|
+
const status = Number(err && (err.status || err.statusCode)) || 0;
|
|
105
|
+
if (status === 400 || status === 404) return true;
|
|
106
|
+
return /document not found|relative document path requires|document path must be a file|unsupported document type/i
|
|
107
|
+
.test(String(err && err.message || ''));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function recoverFailedDocumentReviewNavigation(err, opts, previousHash) {
|
|
111
|
+
if (!isRecoverableDocumentOpenError(err)) return false;
|
|
112
|
+
const currentHash = String(location.hash || '');
|
|
113
|
+
const sessionId = opts?.sessionId || sessionIdFromHash(currentHash) || sessionIdFromHash(previousHash);
|
|
114
|
+
if (activateSessionRoute(sessionId)) return true;
|
|
115
|
+
if (previousHash && previousHash !== currentHash && !String(previousHash).startsWith('#review&type=doc')) {
|
|
116
|
+
replaceHash(previousHash);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (currentHash.startsWith('#review') || currentHash === '#codereview') {
|
|
120
|
+
replaceHash('#review');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
57
126
|
function markdownToSafeHtml(markdown) {
|
|
58
127
|
const text = String(markdown || '');
|
|
59
128
|
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
|
@@ -125,30 +194,347 @@ CR.loadPinnedProjects = function(prefs) {
|
|
|
125
194
|
if (prefs && Array.isArray(prefs)) pinnedProjects = prefs;
|
|
126
195
|
};
|
|
127
196
|
|
|
197
|
+
function homeShortPath(value) {
|
|
198
|
+
return String(value || '').replace(/^\/Users\/[^/]+/, '~');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function reviewTargetInfo(project) {
|
|
202
|
+
const projectPath = String(project?.path || project?.cwd || '');
|
|
203
|
+
const name = String(project?.name || basename(projectPath) || 'folder');
|
|
204
|
+
const branch = String(project?.branch || '');
|
|
205
|
+
const claudeMatch = projectPath.match(/^(.*)\/\.claude\/worktrees\/([^/]+)(?:\/(.*))?$/);
|
|
206
|
+
const walleMatch = projectPath.match(/^(.*)\/\.walle\/worktrees\/([^/]+)(?:\/(.*))?$/);
|
|
207
|
+
const genericMatch = projectPath.match(/^(.*)\/\.worktrees\/([^/]+)(?:\/(.*))?$/);
|
|
208
|
+
const match = claudeMatch || walleMatch || genericMatch;
|
|
209
|
+
|
|
210
|
+
if (match) {
|
|
211
|
+
const provider = match === claudeMatch ? 'Claude' : match === walleMatch ? 'Wall-E' : '';
|
|
212
|
+
const repoRoot = match[1] || '';
|
|
213
|
+
const worktreeName = match[2] || name;
|
|
214
|
+
const innerFolder = match[3] || '';
|
|
215
|
+
return {
|
|
216
|
+
kind: match === claudeMatch ? 'claude-worktree' : match === walleMatch ? 'walle-worktree' : 'worktree',
|
|
217
|
+
label: provider ? `${provider} worktree` : 'Worktree',
|
|
218
|
+
isWorktree: true,
|
|
219
|
+
displayName: name,
|
|
220
|
+
worktreeName,
|
|
221
|
+
repoName: basename(repoRoot),
|
|
222
|
+
repoRoot,
|
|
223
|
+
innerFolder,
|
|
224
|
+
path: projectPath,
|
|
225
|
+
shortPath: homeShortPath(projectPath),
|
|
226
|
+
branch,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const looksLikeGit = !!branch;
|
|
231
|
+
return {
|
|
232
|
+
kind: looksLikeGit && branch === 'main' ? 'main-checkout' : looksLikeGit ? 'git-folder' : 'folder',
|
|
233
|
+
label: looksLikeGit && branch === 'main' ? 'Main checkout' : looksLikeGit ? 'Git folder' : 'Folder',
|
|
234
|
+
isWorktree: false,
|
|
235
|
+
displayName: name,
|
|
236
|
+
worktreeName: '',
|
|
237
|
+
repoName: name,
|
|
238
|
+
repoRoot: projectPath,
|
|
239
|
+
innerFolder: '',
|
|
240
|
+
path: projectPath,
|
|
241
|
+
shortPath: homeShortPath(projectPath),
|
|
242
|
+
branch,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function projectMatchesChooserFilter(project, info) {
|
|
247
|
+
const filter = projectChooserState.filter || 'all';
|
|
248
|
+
if (filter === 'changed') return Number(project.fileCount || 0) > 0;
|
|
249
|
+
if (filter === 'worktrees') return !!info.isWorktree;
|
|
250
|
+
if (filter === 'main') return !info.isWorktree;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function projectMatchesChooserQuery(project, info) {
|
|
255
|
+
const query = String(projectChooserState.query || '').trim().toLowerCase();
|
|
256
|
+
if (!query) return true;
|
|
257
|
+
const sessionsText = (project.sessions || []).map(s => s.label || s.id || '').join(' ');
|
|
258
|
+
const haystack = [
|
|
259
|
+
project.name,
|
|
260
|
+
project.path,
|
|
261
|
+
project.branch,
|
|
262
|
+
info.label,
|
|
263
|
+
info.displayName,
|
|
264
|
+
info.worktreeName,
|
|
265
|
+
info.repoName,
|
|
266
|
+
sessionsText,
|
|
267
|
+
].join(' ').toLowerCase();
|
|
268
|
+
return haystack.includes(query);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function filteredReviewProjects() {
|
|
272
|
+
return (projectChooserState.projects || []).filter((project) => {
|
|
273
|
+
const info = reviewTargetInfo(project);
|
|
274
|
+
return projectMatchesChooserFilter(project, info) && projectMatchesChooserQuery(project, info);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function reviewProjectChooserCounts(projects) {
|
|
279
|
+
const changed = projects.filter(p => Number(p.fileCount || 0) > 0).length;
|
|
280
|
+
const worktrees = projects.filter(p => reviewTargetInfo(p).isWorktree).length;
|
|
281
|
+
return {
|
|
282
|
+
total: projects.length,
|
|
283
|
+
changed,
|
|
284
|
+
worktrees,
|
|
285
|
+
folders: Math.max(0, projects.length - worktrees),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function scheduleProjectChooserRefresh(delayMs) {
|
|
290
|
+
if (projectChooserRefreshTimer) return;
|
|
291
|
+
projectChooserRefreshTimer = setTimeout(() => {
|
|
292
|
+
projectChooserRefreshTimer = null;
|
|
293
|
+
if (crState._view === 'projects' && document.getElementById('codereview-panel')?.classList.contains('active')) {
|
|
294
|
+
CR.showProjectList({ quiet: true });
|
|
295
|
+
}
|
|
296
|
+
}, Math.max(100, Number(delayMs || 1000)));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function updateProjectChooserPolling(scan) {
|
|
300
|
+
if (scan && scan.running) {
|
|
301
|
+
scheduleProjectChooserRefresh(1400);
|
|
302
|
+
} else if (projectChooserRefreshTimer) {
|
|
303
|
+
clearTimeout(projectChooserRefreshTimer);
|
|
304
|
+
projectChooserRefreshTimer = null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderProjectScanStatus() {
|
|
309
|
+
const scan = projectChooserState.scan || {};
|
|
310
|
+
if (scan.running) {
|
|
311
|
+
const existing = (projectChooserState.projects || []).length;
|
|
312
|
+
return `<span class="cr-project-scan-status active"><span class="spinner"></span>${existing ? 'Refreshing counts' : 'Finding projects'}</span>`;
|
|
313
|
+
}
|
|
314
|
+
if (scan.error) {
|
|
315
|
+
return `<span class="cr-project-scan-status error">Refresh failed</span>`;
|
|
316
|
+
}
|
|
317
|
+
if (scan.builtAt) {
|
|
318
|
+
return `<span class="cr-project-scan-status">Updated ${escHtml(formatTimeAgo(scan.builtAt))}</span>`;
|
|
319
|
+
}
|
|
320
|
+
return '';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderReviewProjectFilterButton(filter, label, count) {
|
|
324
|
+
const active = projectChooserState.filter === filter ? ' active' : '';
|
|
325
|
+
const suffix = typeof count === 'number' ? ` <span>${count}</span>` : '';
|
|
326
|
+
return `<button class="cr-project-filter${active}" type="button" data-filter="${escAttr(filter)}" onclick="CR.setProjectFilter('${escAttr(filter)}')">${escHtml(label)}${suffix}</button>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderReviewProjectResults() {
|
|
330
|
+
const projects = filteredReviewProjects();
|
|
331
|
+
if (!projects.length) {
|
|
332
|
+
const query = String(projectChooserState.query || '').trim();
|
|
333
|
+
if (!query && projectChooserState.scan && projectChooserState.scan.running) {
|
|
334
|
+
return `<div class="cr-project-empty-state">
|
|
335
|
+
<div class="cr-project-empty-title">Refreshing review targets</div>
|
|
336
|
+
<div class="cr-project-empty-copy">Project discovery is running in the background. The page stays responsive while git counts load.</div>
|
|
337
|
+
</div>`;
|
|
338
|
+
}
|
|
339
|
+
return `<div class="cr-project-empty-state">
|
|
340
|
+
<div class="cr-project-empty-title">No matching review targets</div>
|
|
341
|
+
<div class="cr-project-empty-copy">${query ? 'Try a folder, worktree, branch, or session name.' : 'Change the filter to see more folders and worktrees.'}</div>
|
|
342
|
+
</div>`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let html = '<div class="cr-project-list">';
|
|
346
|
+
for (const p of projects) {
|
|
347
|
+
const info = reviewTargetInfo(p);
|
|
348
|
+
const sessions = p.sessions || [];
|
|
349
|
+
const hasChanges = Number(p.fileCount || 0) > 0;
|
|
350
|
+
const isPinned = pinnedProjects.includes(p.path);
|
|
351
|
+
const firstSessionId = sessions.length ? sessions[0].id : '';
|
|
352
|
+
const targetLabel = info.isWorktree
|
|
353
|
+
? `${info.label}: ${info.worktreeName}`
|
|
354
|
+
: info.label;
|
|
355
|
+
|
|
356
|
+
const sessionRows = renderReviewProjectSessionRows(p);
|
|
357
|
+
const filesHtml = hasChanges ? `<div class="cr-project-files" aria-label="Changed files">${(p.files || []).map(f =>
|
|
358
|
+
`<span class="cr-project-file">${escHtml(f.path)} <span class="cr-file-add">+${f.additions}</span> <span class="cr-file-del">-${f.deletions}</span></span>`
|
|
359
|
+
).join('')}</div>` : '';
|
|
360
|
+
|
|
361
|
+
html += `<div class="cr-project-card ${hasChanges ? 'has-changes' : ''}${isPinned ? ' pinned' : ''}"
|
|
362
|
+
data-project-path="${escAttr(p.path)}"
|
|
363
|
+
role="button"
|
|
364
|
+
tabindex="0"
|
|
365
|
+
aria-label="Review ${escAttr(targetLabel)}"
|
|
366
|
+
onclick="CR.openReviewForProject('${escAttr(p.path)}', '${escAttr(firstSessionId)}')"
|
|
367
|
+
onkeydown="CR.onProjectCardKey(event, '${escAttr(p.path)}', '${escAttr(firstSessionId)}')"
|
|
368
|
+
${isPinned ? `draggable="true" ondragstart="CR.onPinnedDragStart(event)" ondragover="CR.onPinnedDragOver(event)" ondragleave="CR.onPinnedDragLeave(event)" ondrop="CR.onPinnedDrop(event)"` : ''}>
|
|
369
|
+
<div class="cr-project-card-header">
|
|
370
|
+
<div class="cr-project-card-left">
|
|
371
|
+
${isPinned ? '<span class="cr-drag-handle" title="Drag to reorder">⋮</span>' : ''}
|
|
372
|
+
<button class="cr-pin-btn ${isPinned ? 'pinned' : ''}" onclick="event.stopPropagation(); CR.togglePinProject('${escAttr(p.path)}')" title="${isPinned ? 'Unpin target' : 'Pin target'}">📌</button>
|
|
373
|
+
<div class="cr-project-title-group">
|
|
374
|
+
<span class="cr-project-name">${escHtml(info.displayName)}</span>
|
|
375
|
+
${info.isWorktree ? `<span class="cr-worktree-name" title="${escAttr(info.repoRoot)}">${escHtml(info.worktreeName)}</span>` : ''}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="cr-project-status-row">
|
|
379
|
+
<span class="cr-target-type ${escAttr(info.kind)}">${escHtml(info.label)}</span>
|
|
380
|
+
${p.branch ? `<span class="cr-header-branch">${escHtml(p.branch)}</span>` : ''}
|
|
381
|
+
<span class="cr-project-session-count">${sessions.length} session${sessions.length !== 1 ? 's' : ''}</span>
|
|
382
|
+
${hasChanges ? `<span class="cr-project-badge">${p.fileCount} file${p.fileCount !== 1 ? 's' : ''} changed</span>` : '<span class="cr-project-clean">Clean</span>'}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="cr-project-path-grid">
|
|
386
|
+
<span class="cr-path-label">Folder</span>
|
|
387
|
+
<span class="cr-project-path" title="${escAttr(p.path)}">${escHtml(info.shortPath)}</span>
|
|
388
|
+
${info.isWorktree ? `<span class="cr-path-label">Source</span><span class="cr-project-path" title="${escAttr(info.repoRoot)}">${escHtml(info.repoName || homeShortPath(info.repoRoot))}</span>` : ''}
|
|
389
|
+
</div>
|
|
390
|
+
<div class="cr-project-card-actions">
|
|
391
|
+
<button class="cr-btn primary cr-review-target-btn" type="button" onclick="event.stopPropagation(); CR.openReviewForProject('${escAttr(p.path)}', '${escAttr(firstSessionId)}')">Review this ${info.isWorktree ? 'worktree' : 'folder'}</button>
|
|
392
|
+
<span class="cr-project-action-hint">${info.isWorktree ? 'Exact worktree checkout' : 'Exact folder checkout'}</span>
|
|
393
|
+
</div>
|
|
394
|
+
${sessionRows}
|
|
395
|
+
${filesHtml}
|
|
396
|
+
</div>`;
|
|
397
|
+
}
|
|
398
|
+
html += '</div>';
|
|
399
|
+
return html;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Session labels are derived from a session's first message, which often leaks CTM/agent
|
|
403
|
+
// command wrappers (e.g. <command-message>pua:pua-en</command-message>) and bare URLs.
|
|
404
|
+
// Strip those so the chooser shows a clean, human-readable hint.
|
|
405
|
+
function _cleanReviewLabel(text) {
|
|
406
|
+
let t = String(text || '');
|
|
407
|
+
t = t.replace(/<command-(?:message|name|args|stdout|stderr)>[\s\S]*?<\/command-(?:message|name|args|stdout|stderr)>/gi, ' ');
|
|
408
|
+
t = t.replace(/<\/?[a-z][^>]*>/gi, ' '); // any other stray tags (e.g. unmatched command-* )
|
|
409
|
+
t = t.replace(/\s+/g, ' ').trim();
|
|
410
|
+
return t;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function renderReviewProjectSessionRows(project) {
|
|
414
|
+
const sessions = project.sessions || [];
|
|
415
|
+
if (!sessions.length) return '';
|
|
416
|
+
const maxVisible = 3;
|
|
417
|
+
let html = '<div class="cr-project-session-section"><div class="cr-session-section-label">Optional session context</div><div class="cr-project-session-list">';
|
|
418
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
419
|
+
const s = sessions[i];
|
|
420
|
+
const hidden = i >= maxVisible ? ' hidden' : '';
|
|
421
|
+
const activeTag = s.active ? '<span class="cr-session-active">Active</span>' : '';
|
|
422
|
+
const timeAgo = formatTimeAgo(s.modifiedAt || s.fileModifiedAt);
|
|
423
|
+
html += `<div class="cr-project-session-item${hidden}" data-overflow="${i >= maxVisible ? '1' : '0'}"
|
|
424
|
+
onclick="event.stopPropagation(); CR.openReviewForProject('${escAttr(project.path)}', '${escAttr(s.id)}')"
|
|
425
|
+
title="${escAttr(s.id)}">
|
|
426
|
+
${activeTag}
|
|
427
|
+
<span class="cr-session-label">${escHtml(_cleanReviewLabel(s.label) || s.id.slice(0, 8))}</span>
|
|
428
|
+
<span class="cr-session-time">${escHtml(timeAgo)}</span>
|
|
429
|
+
</div>`;
|
|
430
|
+
}
|
|
431
|
+
if (sessions.length > maxVisible) {
|
|
432
|
+
html += `<button class="cr-session-toggle" onclick="event.stopPropagation(); CR.toggleSessionList(this)">
|
|
433
|
+
+${sessions.length - maxVisible} more session${sessions.length - maxVisible > 1 ? 's' : ''}
|
|
434
|
+
</button>`;
|
|
435
|
+
}
|
|
436
|
+
html += '</div></div>';
|
|
437
|
+
return html;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderReviewProjectChooser(projects) {
|
|
441
|
+
const counts = reviewProjectChooserCounts(projects);
|
|
442
|
+
return `<div class="cr-project-picker">
|
|
443
|
+
<div class="cr-project-chooser-header">
|
|
444
|
+
<div class="cr-project-chooser-copy">
|
|
445
|
+
<div class="cr-picker-eyebrow">Review</div>
|
|
446
|
+
<h2>Choose folder or worktree</h2>
|
|
447
|
+
<p>Select the exact checkout to review. Session rows are optional context; the folder or worktree is the review target.</p>
|
|
448
|
+
</div>
|
|
449
|
+
${renderProjectScanStatus()}
|
|
450
|
+
</div>
|
|
451
|
+
<div class="cr-project-toolbar">
|
|
452
|
+
<label class="cr-project-search">
|
|
453
|
+
<span class="cr-project-search-icon">⌕</span>
|
|
454
|
+
<input type="search" value="${escAttr(projectChooserState.query)}" placeholder="Search folders, worktrees, branches, sessions..." aria-label="Search review targets" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" oninput="CR.setProjectSearch(this.value)">
|
|
455
|
+
</label>
|
|
456
|
+
<div class="cr-project-filters" aria-label="Filter review targets">
|
|
457
|
+
${renderReviewProjectFilterButton('all', 'All', counts.total)}
|
|
458
|
+
${renderReviewProjectFilterButton('changed', 'Changed', counts.changed)}
|
|
459
|
+
${renderReviewProjectFilterButton('worktrees', 'Worktrees', counts.worktrees)}
|
|
460
|
+
${renderReviewProjectFilterButton('main', 'Folders', counts.folders)}
|
|
461
|
+
</div>
|
|
462
|
+
<span class="cr-project-result-count" id="cr-project-result-count"></span>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="cr-project-results" id="cr-project-results">${renderReviewProjectResults()}</div>
|
|
465
|
+
</div>`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function updateReviewProjectResults() {
|
|
469
|
+
const results = document.getElementById('cr-project-results');
|
|
470
|
+
if (results) results.innerHTML = renderReviewProjectResults();
|
|
471
|
+
const count = document.getElementById('cr-project-result-count');
|
|
472
|
+
if (count) {
|
|
473
|
+
const filtered = filteredReviewProjects().length;
|
|
474
|
+
const total = (projectChooserState.projects || []).length;
|
|
475
|
+
count.textContent = filtered === total ? `${total} targets` : `${filtered} of ${total}`;
|
|
476
|
+
}
|
|
477
|
+
document.querySelectorAll('.cr-project-filter').forEach((button) => {
|
|
478
|
+
button.classList.toggle('active', button.dataset.filter === projectChooserState.filter);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
CR.setProjectSearch = function(value) {
|
|
483
|
+
projectChooserState.query = String(value || '');
|
|
484
|
+
updateReviewProjectResults();
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
CR.setProjectFilter = function(filter) {
|
|
488
|
+
projectChooserState.filter = ['all', 'changed', 'worktrees', 'main'].includes(filter) ? filter : 'all';
|
|
489
|
+
updateReviewProjectResults();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
CR.onProjectCardKey = function(event, projectPath, sessionId) {
|
|
493
|
+
if (!event || (event.key !== 'Enter' && event.key !== ' ')) return;
|
|
494
|
+
event.preventDefault();
|
|
495
|
+
CR.openReviewForProject(projectPath, sessionId || '');
|
|
496
|
+
};
|
|
497
|
+
|
|
128
498
|
// --- Show project list (default view when panel opens) ---
|
|
129
|
-
CR.showProjectList = async function() {
|
|
499
|
+
CR.showProjectList = async function(options = {}) {
|
|
130
500
|
const seq = ++crState._projectSeq;
|
|
501
|
+
const quiet = !!options.quiet;
|
|
131
502
|
crState._view = 'projects';
|
|
132
503
|
crState.reviewType = 'code';
|
|
133
504
|
crState.reviewId = null;
|
|
134
505
|
crState.document = null;
|
|
506
|
+
crState.documentCandidates = null;
|
|
135
507
|
crState.activeBlock = null;
|
|
136
508
|
const header = document.getElementById('cr-header');
|
|
137
509
|
const tree = document.getElementById('cr-file-tree');
|
|
138
510
|
const area = document.getElementById('cr-diff-area');
|
|
139
511
|
const footer = document.getElementById('cr-footer');
|
|
140
512
|
|
|
141
|
-
|
|
513
|
+
// Chooser view has no file tree — hide the empty left rail (restored on open).
|
|
514
|
+
document.getElementById('codereview-panel')?.classList.add('cr-choosing');
|
|
515
|
+
if (header) header.innerHTML = '<span class="cr-header-title">Review</span>';
|
|
142
516
|
if (tree) tree.innerHTML = '';
|
|
143
517
|
if (footer) footer.innerHTML = '';
|
|
144
|
-
if (area
|
|
518
|
+
if (area && (!quiet || !(projectChooserState.projects || []).length)) {
|
|
519
|
+
area.innerHTML = projectChooserState.projects.length
|
|
520
|
+
? renderReviewProjectChooser(projectChooserState.projects)
|
|
521
|
+
: '<div class="cr-loading"><span class="spinner"></span> Loading cached review targets...</div>';
|
|
522
|
+
}
|
|
145
523
|
|
|
146
524
|
try {
|
|
147
525
|
const data = await api('/reviews/tracked-projects');
|
|
148
526
|
if (seq !== crState._projectSeq || crState._view !== 'projects') return;
|
|
149
527
|
const projects = data.projects || [];
|
|
528
|
+
projectChooserState.scan = data.scan || null;
|
|
150
529
|
|
|
151
530
|
if (projects.length === 0) {
|
|
531
|
+
if (projectChooserState.scan && projectChooserState.scan.running) {
|
|
532
|
+
projectChooserState.projects = [];
|
|
533
|
+
if (area) area.innerHTML = renderReviewProjectChooser([]);
|
|
534
|
+
updateReviewProjectResults();
|
|
535
|
+
updateProjectChooserPolling(projectChooserState.scan);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
152
538
|
if (area) area.innerHTML = `<div class="cr-diff-empty">
|
|
153
539
|
<div style="text-align:center">
|
|
154
540
|
<div style="font-size:32px;opacity:0.3;margin-bottom:12px">📁</div>
|
|
@@ -170,63 +556,14 @@ CR.showProjectList = async function() {
|
|
|
170
556
|
return 0; // Keep original (most recent) order for unpinned
|
|
171
557
|
});
|
|
172
558
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const isPinned = pinnedProjects.includes(p.path);
|
|
178
|
-
const firstSessionId = p.sessions.length ? p.sessions[0].id : '';
|
|
179
|
-
|
|
180
|
-
// Sessions list (collapsed if > 3, show all with toggle)
|
|
181
|
-
const maxVisible = 3;
|
|
182
|
-
let sessionsHtml = '';
|
|
183
|
-
if (p.sessions.length > 0) {
|
|
184
|
-
sessionsHtml = '<div class="cr-project-session-list">';
|
|
185
|
-
for (let i = 0; i < p.sessions.length; i++) {
|
|
186
|
-
const s = p.sessions[i];
|
|
187
|
-
const hidden = i >= maxVisible ? ' hidden' : '';
|
|
188
|
-
const activeTag = s.active ? '<span class="cr-session-active">Active</span>' : '';
|
|
189
|
-
const timeAgo = formatTimeAgo(s.modifiedAt);
|
|
190
|
-
sessionsHtml += `<div class="cr-project-session-item${hidden}" data-overflow="${i >= maxVisible ? '1' : '0'}"
|
|
191
|
-
onclick="event.stopPropagation(); CR.openReviewForProject('${escAttr(p.path)}', '${escAttr(s.id)}')"
|
|
192
|
-
title="${escAttr(s.id)}">
|
|
193
|
-
${activeTag}
|
|
194
|
-
<span class="cr-session-label">${escHtml(s.label)}</span>
|
|
195
|
-
<span class="cr-session-time">${escHtml(timeAgo)}</span>
|
|
196
|
-
</div>`;
|
|
197
|
-
}
|
|
198
|
-
if (p.sessions.length > maxVisible) {
|
|
199
|
-
sessionsHtml += `<button class="cr-session-toggle" onclick="event.stopPropagation(); CR.toggleSessionList(this)">
|
|
200
|
-
+${p.sessions.length - maxVisible} more session${p.sessions.length - maxVisible > 1 ? 's' : ''}
|
|
201
|
-
</button>`;
|
|
202
|
-
}
|
|
203
|
-
sessionsHtml += '</div>';
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
html += `<div class="cr-project-card ${hasChanges ? 'has-changes' : ''}${isPinned ? ' pinned' : ''}"
|
|
207
|
-
data-project-path="${escAttr(p.path)}"
|
|
208
|
-
onclick="CR.openReviewForProject('${escAttr(p.path)}', '${escAttr(firstSessionId)}')"
|
|
209
|
-
${isPinned ? `draggable="true" ondragstart="CR.onPinnedDragStart(event)" ondragover="CR.onPinnedDragOver(event)" ondragleave="CR.onPinnedDragLeave(event)" ondrop="CR.onPinnedDrop(event)"` : ''}>
|
|
210
|
-
<div class="cr-project-card-header">
|
|
211
|
-
${isPinned ? '<span class="cr-drag-handle" title="Drag to reorder">⋮</span>' : ''}
|
|
212
|
-
<button class="cr-pin-btn ${isPinned ? 'pinned' : ''}" onclick="event.stopPropagation(); CR.togglePinProject('${escAttr(p.path)}')" title="${isPinned ? 'Unpin project' : 'Pin to top'}">📌</button>
|
|
213
|
-
<span class="cr-project-name">${escHtml(p.name)}</span>
|
|
214
|
-
${p.branch ? `<span class="cr-header-branch">${escHtml(p.branch)}</span>` : ''}
|
|
215
|
-
<span class="cr-project-session-count">${p.sessions.length} session${p.sessions.length !== 1 ? 's' : ''}</span>
|
|
216
|
-
${hasChanges ? `<span class="cr-project-badge">${p.fileCount} file${p.fileCount !== 1 ? 's' : ''} changed</span>` : '<span class="cr-project-clean">Clean</span>'}
|
|
217
|
-
</div>
|
|
218
|
-
<div class="cr-project-path">${escHtml(p.path)}</div>
|
|
219
|
-
${sessionsHtml}
|
|
220
|
-
${hasChanges ? `<div class="cr-project-files">${p.files.map(f =>
|
|
221
|
-
`<span class="cr-project-file">${escHtml(f.path)} <span class="cr-file-add">+${f.additions}</span> <span class="cr-file-del">-${f.deletions}</span></span>`
|
|
222
|
-
).join('')}</div>` : ''}
|
|
223
|
-
</div>`;
|
|
224
|
-
}
|
|
225
|
-
html += '</div>';
|
|
226
|
-
if (area) area.innerHTML = html;
|
|
559
|
+
projectChooserState.projects = projects;
|
|
560
|
+
if (area) area.innerHTML = renderReviewProjectChooser(projects);
|
|
561
|
+
updateReviewProjectResults();
|
|
562
|
+
updateProjectChooserPolling(projectChooserState.scan);
|
|
227
563
|
} catch (e) {
|
|
228
564
|
if (seq !== crState._projectSeq || crState._view !== 'projects') return;
|
|
229
565
|
if (area) area.innerHTML = `<div class="cr-diff-empty" style="color:var(--red)">Failed to load projects: ${escHtml(e.message)}</div>`;
|
|
566
|
+
updateProjectChooserPolling(null);
|
|
230
567
|
}
|
|
231
568
|
};
|
|
232
569
|
|
|
@@ -281,10 +618,14 @@ CR.openReview = async function(sessionId, projectPath) {
|
|
|
281
618
|
crState.baseRef = '';
|
|
282
619
|
crState.files = [];
|
|
283
620
|
crState.comments = [];
|
|
621
|
+
crState.viewedFiles = new Set();
|
|
622
|
+
try { crState.diffMode = localStorage.getItem('ctm.review.diffMode') === 'split' ? 'split' : 'unified'; } catch {}
|
|
284
623
|
crState.activeFile = null;
|
|
285
624
|
crState.reviewId = null;
|
|
286
625
|
crState.document = null;
|
|
626
|
+
crState.documentCandidates = null;
|
|
287
627
|
crState.activeBlock = null;
|
|
628
|
+
document.getElementById('codereview-panel')?.classList.remove('cr-choosing');
|
|
288
629
|
|
|
289
630
|
// Show panel
|
|
290
631
|
if (typeof window.activateTab === 'function') {
|
|
@@ -296,7 +637,7 @@ CR.openReview = async function(sessionId, projectPath) {
|
|
|
296
637
|
}
|
|
297
638
|
|
|
298
639
|
// Update URL hash with folder info
|
|
299
|
-
const hashParts = ['#
|
|
640
|
+
const hashParts = ['#review'];
|
|
300
641
|
if (projectPath) hashParts.push('project=' + encodeURIComponent(projectPath));
|
|
301
642
|
if (sessionId) hashParts.push('session=' + encodeURIComponent(sessionId));
|
|
302
643
|
history.replaceState(null, '', location.pathname + location.search + hashParts.join('&'));
|
|
@@ -305,22 +646,28 @@ CR.openReview = async function(sessionId, projectPath) {
|
|
|
305
646
|
renderLoading();
|
|
306
647
|
|
|
307
648
|
try {
|
|
308
|
-
// Load branch, commits,
|
|
309
|
-
const [branchData, commitsData, logData] = await Promise.all([
|
|
649
|
+
// Load branch, commits, rich commit log, and the default review base in parallel
|
|
650
|
+
const [branchData, commitsData, logData, baseInfo] = await Promise.all([
|
|
310
651
|
api(`/reviews/branch?project=${encodeURIComponent(projectPath)}`),
|
|
311
652
|
api(`/reviews/commits?project=${encodeURIComponent(projectPath)}`),
|
|
312
653
|
api(`/reviews/commit-log?project=${encodeURIComponent(projectPath)}&count=20`),
|
|
654
|
+
api(`/reviews/base-info?project=${encodeURIComponent(projectPath)}`).catch(() => null),
|
|
313
655
|
]);
|
|
314
656
|
if (seq !== crState._openSeq || crState._view !== 'review') return;
|
|
315
657
|
crState.branch = branchData.branch || '';
|
|
316
658
|
crState._commitLog = logData.commits || [];
|
|
659
|
+
crState._baseInfo = baseInfo;
|
|
660
|
+
// Default to "all branch work vs main" (PR model); fall back through uncommitted →
|
|
661
|
+
// working tree → staged to the first base that actually has changes, so a freshly
|
|
662
|
+
// opened project never lands on a misleading empty "No changes detected" screen.
|
|
663
|
+
crState.baseRef = pickInitialBaseRef(baseInfo);
|
|
317
664
|
renderHeader(commitsData.commits || []);
|
|
318
665
|
|
|
319
666
|
// Create review record in DB
|
|
320
667
|
const { id } = await api('/reviews', 'POST', {
|
|
321
668
|
session_id: sessionId,
|
|
322
669
|
project_path: projectPath,
|
|
323
|
-
base_ref:
|
|
670
|
+
base_ref: crState.baseRef,
|
|
324
671
|
});
|
|
325
672
|
if (seq !== crState._openSeq || crState._view !== 'review') return;
|
|
326
673
|
crState.reviewId = id;
|
|
@@ -336,11 +683,13 @@ CR.openReview = async function(sessionId, projectPath) {
|
|
|
336
683
|
};
|
|
337
684
|
|
|
338
685
|
CR.openDocumentReview = async function(filePath, line, opts) {
|
|
686
|
+
opts = opts || {};
|
|
339
687
|
const seq = ++crState._openSeq;
|
|
340
688
|
const targetLine = Math.max(1, Number(line) || 1);
|
|
689
|
+
const previousHash = location.hash || '';
|
|
341
690
|
crState._view = 'review';
|
|
342
691
|
crState.reviewType = 'doc';
|
|
343
|
-
crState.sessionId = opts
|
|
692
|
+
crState.sessionId = opts.sessionId || null;
|
|
344
693
|
crState.projectPath = null;
|
|
345
694
|
crState.baseRef = '';
|
|
346
695
|
crState.branch = '';
|
|
@@ -349,8 +698,10 @@ CR.openDocumentReview = async function(filePath, line, opts) {
|
|
|
349
698
|
crState.activeFile = filePath || null;
|
|
350
699
|
crState.reviewId = null;
|
|
351
700
|
crState.document = null;
|
|
701
|
+
crState.documentCandidates = null;
|
|
352
702
|
crState.activeBlock = null;
|
|
353
703
|
crState.docLine = targetLine;
|
|
704
|
+
document.getElementById('codereview-panel')?.classList.remove('cr-choosing');
|
|
354
705
|
|
|
355
706
|
if (typeof window.activateTab === 'function') {
|
|
356
707
|
if (!window._ctmState.tabOrder.includes('codereview')) {
|
|
@@ -360,9 +711,13 @@ CR.openDocumentReview = async function(filePath, line, opts) {
|
|
|
360
711
|
if (typeof window.renderTabs === 'function') window.renderTabs();
|
|
361
712
|
}
|
|
362
713
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
714
|
+
const routeParams = new URLSearchParams();
|
|
715
|
+
routeParams.set('type', 'doc');
|
|
716
|
+
routeParams.set('path', filePath || '');
|
|
717
|
+
routeParams.set('line', String(targetLine));
|
|
718
|
+
if (opts.cwd) routeParams.set('cwd', opts.cwd);
|
|
719
|
+
if (crState.sessionId) routeParams.set('session', crState.sessionId);
|
|
720
|
+
history.replaceState(null, '', location.pathname + location.search + '#review&' + routeParams.toString());
|
|
366
721
|
|
|
367
722
|
renderDocHeader();
|
|
368
723
|
renderLoading();
|
|
@@ -371,27 +726,103 @@ CR.openDocumentReview = async function(filePath, line, opts) {
|
|
|
371
726
|
const data = await api('/reviews/document-review', 'POST', {
|
|
372
727
|
path: filePath,
|
|
373
728
|
line: targetLine,
|
|
729
|
+
cwd: opts.cwd || undefined,
|
|
374
730
|
session_id: crState.sessionId || undefined,
|
|
375
731
|
});
|
|
376
732
|
if (seq !== crState._openSeq || crState.reviewType !== 'doc') return;
|
|
377
733
|
crState.reviewId = data.id;
|
|
378
734
|
crState.document = data.document || null;
|
|
735
|
+
crState.documentCandidates = null;
|
|
379
736
|
crState.projectPath = crState.document?.projectRoot || data.review?.project_path || null;
|
|
380
737
|
crState.activeFile = crState.document?.path || filePath;
|
|
381
738
|
crState.comments = data.review?.comments || [];
|
|
382
739
|
const targetBlock = findDocBlockForLine(crState.document?.line || targetLine);
|
|
383
740
|
crState.activeBlock = targetBlock?.id || null;
|
|
741
|
+
if (crState.document?.path) {
|
|
742
|
+
const canonicalParams = new URLSearchParams();
|
|
743
|
+
canonicalParams.set('type', 'doc');
|
|
744
|
+
canonicalParams.set('path', crState.document.path);
|
|
745
|
+
canonicalParams.set('line', String(crState.document.line || targetLine));
|
|
746
|
+
if (crState.sessionId) canonicalParams.set('session', crState.sessionId);
|
|
747
|
+
history.replaceState(null, '', location.pathname + location.search + '#review&' + canonicalParams.toString());
|
|
748
|
+
}
|
|
384
749
|
renderDocHeader();
|
|
385
750
|
renderDocTree();
|
|
386
751
|
renderDocReview();
|
|
387
752
|
} catch (e) {
|
|
388
753
|
if (seq !== crState._openSeq || crState.reviewType !== 'doc') return;
|
|
754
|
+
if (e?.data?.code === 'EAMBIGUOUS' && Array.isArray(e.data.candidates) && e.data.candidates.length) {
|
|
755
|
+
renderDocumentCandidateChooser(filePath, targetLine, opts, e.data.candidates);
|
|
756
|
+
if (typeof window.toast === 'function') window.toast('Choose which document to review', { type: 'info' });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
389
759
|
const area = document.getElementById('cr-diff-area');
|
|
390
760
|
if (area) area.innerHTML = `<div class="cr-diff-empty" style="color:var(--red)">Failed to open document review: ${escHtml(e.message)}</div>`;
|
|
391
761
|
if (typeof window.toast === 'function') window.toast('Failed to open document review: ' + e.message, { type: 'error' });
|
|
762
|
+
recoverFailedDocumentReviewNavigation(e, opts, previousHash);
|
|
392
763
|
}
|
|
393
764
|
};
|
|
394
765
|
|
|
766
|
+
CR.openDocumentReference = async function(rawReference, opts) {
|
|
767
|
+
const parsed = window.CTMDocLinks && typeof window.CTMDocLinks.splitReference === 'function'
|
|
768
|
+
? window.CTMDocLinks.splitReference(rawReference, opts && opts.line)
|
|
769
|
+
: { path: String(rawReference || ''), line: opts && opts.line || 1 };
|
|
770
|
+
return CR.openDocumentReview(parsed.path, parsed.line, opts || {});
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
CR.openDocumentCandidate = function(index) {
|
|
774
|
+
const state = crState.documentCandidates;
|
|
775
|
+
const candidate = state && Array.isArray(state.candidates) ? state.candidates[index] : null;
|
|
776
|
+
if (!candidate || !candidate.path) return;
|
|
777
|
+
const opts = Object.assign({}, state.opts || {});
|
|
778
|
+
delete opts.cwd;
|
|
779
|
+
return CR.openDocumentReview(candidate.path, state.line || 1, opts);
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
function renderDocumentCandidateChooser(rawPath, line, opts, candidates) {
|
|
783
|
+
crState.documentCandidates = {
|
|
784
|
+
rawPath: rawPath || '',
|
|
785
|
+
line: Math.max(1, Number(line) || 1),
|
|
786
|
+
opts: Object.assign({}, opts || {}),
|
|
787
|
+
candidates: candidates.slice(0, 50),
|
|
788
|
+
};
|
|
789
|
+
crState.document = null;
|
|
790
|
+
crState.projectPath = candidates[0]?.projectRoot || null;
|
|
791
|
+
crState.comments = [];
|
|
792
|
+
crState.activeBlock = null;
|
|
793
|
+
renderDocHeader();
|
|
794
|
+
const tree = document.getElementById('cr-file-tree');
|
|
795
|
+
const footer = document.getElementById('cr-footer');
|
|
796
|
+
const area = document.getElementById('cr-diff-area');
|
|
797
|
+
if (tree) {
|
|
798
|
+
tree.innerHTML = candidates.slice(0, 50).map((candidate, index) => `
|
|
799
|
+
<div class="cr-file-item ${index === 0 ? 'active' : ''}" onclick="CR.openDocumentCandidate(${index})">
|
|
800
|
+
<span class="cr-file-icon">MD</span>
|
|
801
|
+
<span class="cr-file-name">${escHtml(candidate.relativePath || candidate.path || '')}</span>
|
|
802
|
+
</div>
|
|
803
|
+
`).join('');
|
|
804
|
+
}
|
|
805
|
+
if (footer) footer.innerHTML = '';
|
|
806
|
+
if (!area) return;
|
|
807
|
+
const title = escHtml(rawPath || 'document');
|
|
808
|
+
const list = candidates.slice(0, 50).map((candidate, index) => `
|
|
809
|
+
<button class="cr-doc-candidate" type="button" onclick="CR.openDocumentCandidate(${index})">
|
|
810
|
+
<span class="cr-doc-candidate-name">${escHtml(candidate.relativePath || basename(candidate.path))}</span>
|
|
811
|
+
<span class="cr-doc-candidate-path">${escHtml(candidate.projectRoot || '')}</span>
|
|
812
|
+
</button>
|
|
813
|
+
`).join('');
|
|
814
|
+
area.innerHTML = `
|
|
815
|
+
<div class="cr-doc-candidate-shell">
|
|
816
|
+
<div class="cr-doc-candidate-panel">
|
|
817
|
+
<div class="cr-doc-candidate-eyebrow">Document Review</div>
|
|
818
|
+
<h2>Choose the document for ${title}</h2>
|
|
819
|
+
<p>The reference matches multiple Markdown/text files in this project. Pick the artifact to review; CTM will then canonicalize the route to the exact file.</p>
|
|
820
|
+
<div class="cr-doc-candidate-list">${list}</div>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
|
|
395
826
|
async function loadDiff() {
|
|
396
827
|
const seq = ++crState._diffSeq;
|
|
397
828
|
renderLoading();
|
|
@@ -407,6 +838,7 @@ async function loadDiff() {
|
|
|
407
838
|
api(`/reviews/${crState.reviewId}`, 'PUT', { file_count: crState.files.length }).catch(() => {});
|
|
408
839
|
}
|
|
409
840
|
|
|
841
|
+
_loadViewedFiles();
|
|
410
842
|
if (crState.files.length === 0) {
|
|
411
843
|
renderNoChanges();
|
|
412
844
|
} else {
|
|
@@ -431,15 +863,108 @@ CR.changeBase = async function(value) {
|
|
|
431
863
|
|
|
432
864
|
CR.refreshDiff = function() { loadDiff(); };
|
|
433
865
|
|
|
866
|
+
// Diff layout: 'unified' (default) or 'split' (side-by-side). Persisted globally.
|
|
867
|
+
CR.setDiffMode = function(mode) {
|
|
868
|
+
mode = mode === 'split' ? 'split' : 'unified';
|
|
869
|
+
if (crState.diffMode === mode) return;
|
|
870
|
+
crState.diffMode = mode;
|
|
871
|
+
try { localStorage.setItem('ctm.review.diffMode', mode); } catch {}
|
|
872
|
+
renderHeader();
|
|
873
|
+
if (crState.reviewType === 'code' && crState.files.length) renderDiff();
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// --- "Viewed" file state + review progress (persisted per review+base in localStorage) ---
|
|
877
|
+
function _viewedStorageKey() {
|
|
878
|
+
// Key by project + base (stable across reopens) rather than the ephemeral review row id,
|
|
879
|
+
// which is recreated on each open and would lose viewed-state between sessions.
|
|
880
|
+
return `ctm.review.viewed.${crState.projectPath || 'none'}.${crState.baseRef || 'wt'}`;
|
|
881
|
+
}
|
|
882
|
+
function _loadViewedFiles() {
|
|
883
|
+
crState.viewedFiles = new Set();
|
|
884
|
+
try {
|
|
885
|
+
const raw = localStorage.getItem(_viewedStorageKey());
|
|
886
|
+
if (raw) {
|
|
887
|
+
const arr = JSON.parse(raw);
|
|
888
|
+
const present = new Set(crState.files.map(f => f.path));
|
|
889
|
+
// Drop stale entries for files no longer in the diff (e.g. after a base change).
|
|
890
|
+
for (const p of arr) if (present.has(p)) crState.viewedFiles.add(p);
|
|
891
|
+
}
|
|
892
|
+
} catch {}
|
|
893
|
+
}
|
|
894
|
+
function _saveViewedFiles() {
|
|
895
|
+
try { localStorage.setItem(_viewedStorageKey(), JSON.stringify([...crState.viewedFiles])); } catch {}
|
|
896
|
+
}
|
|
897
|
+
CR.toggleViewed = function(filePath, ev) {
|
|
898
|
+
if (ev) { ev.stopPropagation(); ev.preventDefault(); }
|
|
899
|
+
if (crState.viewedFiles.has(filePath)) crState.viewedFiles.delete(filePath);
|
|
900
|
+
else crState.viewedFiles.add(filePath);
|
|
901
|
+
_saveViewedFiles();
|
|
902
|
+
// Update just the affected DOM: the tree item + the diff section collapse + progress.
|
|
903
|
+
renderFileTree();
|
|
904
|
+
const section = document.querySelector(`.cr-diff-file-section[data-file-path="${CSS.escape(filePath)}"]`);
|
|
905
|
+
if (section) section.classList.toggle('viewed', crState.viewedFiles.has(filePath));
|
|
906
|
+
_renderReviewProgress();
|
|
907
|
+
};
|
|
908
|
+
CR.markAllViewed = function(viewed) {
|
|
909
|
+
crState.viewedFiles = viewed ? new Set(crState.files.map(f => f.path)) : new Set();
|
|
910
|
+
_saveViewedFiles();
|
|
911
|
+
renderFileTree();
|
|
912
|
+
renderDiff();
|
|
913
|
+
};
|
|
914
|
+
// Progress indicator ("N / M reviewed" + bar) lives in the footer; updated in place.
|
|
915
|
+
function _renderReviewProgress() {
|
|
916
|
+
const el = document.getElementById('cr-progress');
|
|
917
|
+
if (!el) return;
|
|
918
|
+
const total = crState.files.length;
|
|
919
|
+
const done = crState.files.filter(f => crState.viewedFiles.has(f.path)).length;
|
|
920
|
+
const pct = total ? Math.round((done / total) * 100) : 0;
|
|
921
|
+
el.innerHTML = total
|
|
922
|
+
? `<span class="cr-progress-label">${done} / ${total} reviewed</span>
|
|
923
|
+
<span class="cr-progress-bar"><span class="cr-progress-fill" style="width:${pct}%"></span></span>`
|
|
924
|
+
: '';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Order the "working-set" bases by preference for the initial view, given base-info counts.
|
|
928
|
+
// On a feature branch: all-branch-work → all-uncommitted → working tree → staged.
|
|
929
|
+
// On main/detached: all-uncommitted → working tree → staged.
|
|
930
|
+
function _workingBaseOrder(baseInfo) {
|
|
931
|
+
const c = (baseInfo && baseInfo.counts) || {};
|
|
932
|
+
const vsMain = ['--vs-main', 'All changes vs ' + ((baseInfo && baseInfo.mainBranch) || 'main'), c['vs-main'] || 0];
|
|
933
|
+
const rest = [
|
|
934
|
+
['--uncommitted', 'All uncommitted', c['uncommitted'] || 0],
|
|
935
|
+
['', 'Working tree (unstaged)', c['working'] || 0],
|
|
936
|
+
['--staged', 'Staged changes', c['staged'] || 0],
|
|
937
|
+
];
|
|
938
|
+
return (baseInfo && baseInfo.kind === 'vs-main') ? [vsMain, ...rest] : rest;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Choose the initial base ref: the first working-set base with a non-zero count, else the
|
|
942
|
+
// server's default sentinel (so a truly-clean repo still shows an honest empty state).
|
|
943
|
+
function pickInitialBaseRef(baseInfo) {
|
|
944
|
+
if (!baseInfo) return '';
|
|
945
|
+
const firstNonEmpty = _workingBaseOrder(baseInfo).find(([, , n]) => n > 0);
|
|
946
|
+
return firstNonEmpty ? firstNonEmpty[0] : (baseInfo.defaultBase || '');
|
|
947
|
+
}
|
|
948
|
+
|
|
434
949
|
// --- Render functions ---
|
|
435
950
|
|
|
436
951
|
function renderHeader(commits) {
|
|
437
952
|
const h = document.getElementById('cr-header');
|
|
438
953
|
if (!h) return;
|
|
439
|
-
const backLink = `<a class="cr-back-link" onclick="CR.backToSession()">←
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
:
|
|
954
|
+
const backLink = `<a class="cr-back-link" onclick="CR.backToSession()">← Change target</a>`;
|
|
955
|
+
const targetInfo = reviewTargetInfo({
|
|
956
|
+
path: crState.projectPath,
|
|
957
|
+
name: basename(crState.projectPath),
|
|
958
|
+
branch: crState.branch,
|
|
959
|
+
});
|
|
960
|
+
const targetMeta = targetInfo.isWorktree
|
|
961
|
+
? `${targetInfo.label} · ${targetInfo.worktreeName}`
|
|
962
|
+
: targetInfo.label;
|
|
963
|
+
const targetHtml = `<div class="cr-review-target" title="${escAttr(crState.projectPath || '')}">
|
|
964
|
+
<span class="cr-review-target-kicker">Target</span>
|
|
965
|
+
<span class="cr-review-target-name">${escHtml(targetInfo.displayName)}</span>
|
|
966
|
+
<span class="cr-review-target-meta">${escHtml(targetMeta)}${crState.branch ? ` · ${escHtml(crState.branch)}` : ''}</span>
|
|
967
|
+
</div>`;
|
|
443
968
|
const modeTabs = `<div class="cr-review-mode-tabs" aria-label="Review type">
|
|
444
969
|
<button class="cr-review-mode-btn active" type="button">Code</button>
|
|
445
970
|
<button class="cr-review-mode-btn" type="button" onclick="CR.showDocumentOpenState()" title="Open a Markdown document URL to review docs">Docs</button>
|
|
@@ -449,35 +974,40 @@ function renderHeader(commits) {
|
|
|
449
974
|
// logMap keyed by both full SHA and short SHA for flexible lookup
|
|
450
975
|
const logMap = {};
|
|
451
976
|
for (const lc of (crState._commitLog || [])) { logMap[lc.sha] = lc; logMap[lc.shortSha] = lc; }
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
977
|
+
// Working-set bases first (all branch work / uncommitted / working / staged), each
|
|
978
|
+
// labelled with its file count so the choice is obvious; then a commits group.
|
|
979
|
+
const _countSuffix = (n) => (typeof n === 'number' ? ` (${n})` : '');
|
|
980
|
+
let baseOptions = '';
|
|
981
|
+
for (const [val, label, count] of _workingBaseOrder(crState._baseInfo)) {
|
|
982
|
+
const sel = crState.baseRef === val ? ' selected' : '';
|
|
983
|
+
baseOptions += `<option value="${escAttr(val)}"${sel}>${escHtml(label)}${_countSuffix(count)}</option>`;
|
|
984
|
+
}
|
|
985
|
+
let commitOptions = '';
|
|
986
|
+
const commitList = commits || crState._commits;
|
|
987
|
+
if (commits) crState._commits = commits; // store for later re-render
|
|
988
|
+
if (commitList) {
|
|
989
|
+
for (const c of commitList) {
|
|
456
990
|
const sel = crState.baseRef === c.sha ? ' selected' : '';
|
|
457
991
|
const rich = logMap[c.sha];
|
|
458
992
|
const meta = rich ? ` \u00B7 ${rich.author.split(' ')[0]} \u00B7 ${formatCommitDate(rich.authorDate)}` : '';
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
// Store for later re-render
|
|
462
|
-
crState._commits = commits;
|
|
463
|
-
} else if (crState._commits) {
|
|
464
|
-
for (const c of crState._commits) {
|
|
465
|
-
const sel = crState.baseRef === c.sha ? ' selected' : '';
|
|
466
|
-
const rich = logMap[c.sha];
|
|
467
|
-
const meta = rich ? ` \u00B7 ${rich.author.split(' ')[0]} \u00B7 ${formatCommitDate(rich.authorDate)}` : '';
|
|
468
|
-
baseOptions += `<option value="${escHtml(c.sha)}"${sel}>${escHtml(c.sha.slice(0,7))} ${escHtml(c.message.slice(0,40))}${meta}</option>`;
|
|
993
|
+
commitOptions += `<option value="${escHtml(c.sha)}"${sel}>${escHtml(c.sha.slice(0,7))} ${escHtml(c.message.slice(0,40))}${meta}</option>`;
|
|
469
994
|
}
|
|
470
995
|
}
|
|
996
|
+
const commitGroup = commitOptions ? `<optgroup label="Since a commit">${commitOptions}</optgroup>` : '';
|
|
471
997
|
|
|
472
998
|
h.innerHTML = `
|
|
473
999
|
${backLink}
|
|
474
1000
|
<span class="cr-header-title">Review</span>
|
|
475
1001
|
${modeTabs}
|
|
476
|
-
${
|
|
1002
|
+
${targetHtml}
|
|
477
1003
|
<div class="cr-header-actions">
|
|
478
1004
|
<button class="cr-chat-toggle" onclick="CR.toggleChat()" title="Toggle AI Chat (Cmd+Shift+C)" id="cr-chat-toggle-btn">💬 Chat</button>
|
|
479
|
-
<
|
|
480
|
-
|
|
1005
|
+
<div class="cr-diff-mode-toggle" role="group" aria-label="Diff layout">
|
|
1006
|
+
<button class="cr-diff-mode-btn ${crState.diffMode !== 'split' ? 'active' : ''}" type="button" onclick="CR.setDiffMode('unified')" title="Unified diff">Unified</button>
|
|
1007
|
+
<button class="cr-diff-mode-btn ${crState.diffMode === 'split' ? 'active' : ''}" type="button" onclick="CR.setDiffMode('split')" title="Side-by-side diff">Split</button>
|
|
1008
|
+
</div>
|
|
1009
|
+
<label class="cr-compare-label">Compare:</label>
|
|
1010
|
+
<select class="cr-base-select" onchange="CR.changeBase(this.value)"><optgroup label="Changes">${baseOptions}</optgroup>${commitGroup}</select>
|
|
481
1011
|
<button class="cr-btn" onclick="CR.refreshDiff()" title="Refresh diff">↻ Refresh</button>
|
|
482
1012
|
<div class="send-dropdown-wrap" id="cr-send-dropdown-wrap">
|
|
483
1013
|
<button class="cr-btn primary btn-main" onclick="CR.submitReview()" id="cr-submit-btn" disabled>Send</button>
|
|
@@ -530,6 +1060,7 @@ CR.showDocumentOpenState = function() {
|
|
|
530
1060
|
crState._view = 'review';
|
|
531
1061
|
crState.reviewId = null;
|
|
532
1062
|
crState.document = null;
|
|
1063
|
+
crState.documentCandidates = null;
|
|
533
1064
|
crState.comments = [];
|
|
534
1065
|
renderDocHeader();
|
|
535
1066
|
const tree = document.getElementById('cr-file-tree');
|
|
@@ -577,8 +1108,11 @@ function renderDocTree() {
|
|
|
577
1108
|
for (const block of headings) {
|
|
578
1109
|
const count = docCommentsForBlock(block).filter(c => c.status === 'open').length;
|
|
579
1110
|
const active = crState.activeBlock === block.id;
|
|
1111
|
+
// TOC entry: no leading `#` glyph; hierarchy is shown via padding-left
|
|
1112
|
+
// (cr-doc-level-N classes from CSS). The empty cr-file-icon span is
|
|
1113
|
+
// retained so the existing icon+name+badge layout stays predictable.
|
|
580
1114
|
html += `<div class="cr-file-item cr-doc-heading cr-doc-level-${Math.min(block.level || 1, 6)} ${active ? 'active' : ''}" onclick="CR.selectDocBlock('${escAttr(block.id)}')">
|
|
581
|
-
<span class="cr-file-icon"
|
|
1115
|
+
<span class="cr-file-icon" aria-hidden="true"></span>
|
|
582
1116
|
<span class="cr-file-name" title="${escAttr(block.text)}">${escHtml(block.text)}</span>
|
|
583
1117
|
${count ? `<span class="cr-file-badge">${count}</span>` : ''}
|
|
584
1118
|
</div>`;
|
|
@@ -622,9 +1156,70 @@ function renderDocReview() {
|
|
|
622
1156
|
} else {
|
|
623
1157
|
area.scrollTop = 0;
|
|
624
1158
|
}
|
|
1159
|
+
setupDocScrollSpy(area);
|
|
625
1160
|
renderFooter();
|
|
626
1161
|
}
|
|
627
1162
|
|
|
1163
|
+
// Scroll-spy: highlight the TOC entry for the heading-block currently in
|
|
1164
|
+
// view. Uses IntersectionObserver scoped to the rendered pane so the active
|
|
1165
|
+
// block tracks the user's reading position. Re-runs after every renderDoc
|
|
1166
|
+
// because the DOM is rebuilt each time.
|
|
1167
|
+
let _docScrollSpyObserver = null;
|
|
1168
|
+
function setupDocScrollSpy(area) {
|
|
1169
|
+
if (_docScrollSpyObserver) { try { _docScrollSpyObserver.disconnect(); } catch {} _docScrollSpyObserver = null; }
|
|
1170
|
+
if (!area || typeof IntersectionObserver !== 'function') return;
|
|
1171
|
+
const pane = area.querySelector('.cr-doc-rendered-pane');
|
|
1172
|
+
if (!pane) return;
|
|
1173
|
+
const blocks = Array.from(pane.querySelectorAll('.cr-doc-block'));
|
|
1174
|
+
if (!blocks.length) return;
|
|
1175
|
+
|
|
1176
|
+
// Map block id → its heading block id (for non-heading blocks, attribute
|
|
1177
|
+
// them to the nearest preceding heading so the TOC stays anchored).
|
|
1178
|
+
const headingFor = new Map();
|
|
1179
|
+
let lastHeadingId = null;
|
|
1180
|
+
for (const block of blocks) {
|
|
1181
|
+
const blockId = block.getAttribute('data-block-id');
|
|
1182
|
+
const isHeading = !!block.querySelector('.cr-doc-markdown > h1, .cr-doc-markdown > h2, .cr-doc-markdown > h3, .cr-doc-markdown > h4, .cr-doc-markdown > h5, .cr-doc-markdown > h6');
|
|
1183
|
+
if (isHeading) lastHeadingId = blockId;
|
|
1184
|
+
headingFor.set(blockId, lastHeadingId);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const tree = document.getElementById('cr-file-tree');
|
|
1188
|
+
const setActive = (headingId) => {
|
|
1189
|
+
if (!tree) return;
|
|
1190
|
+
tree.querySelectorAll('.cr-doc-heading.active').forEach(el => el.classList.remove('active'));
|
|
1191
|
+
if (!headingId) return;
|
|
1192
|
+
const escaped = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(headingId) : headingId.replace(/"/g, '');
|
|
1193
|
+
const sel = `.cr-doc-heading[onclick*="${escaped}"]`;
|
|
1194
|
+
const target = tree.querySelector(sel);
|
|
1195
|
+
if (target) target.classList.add('active');
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// Track the topmost intersecting block. Threshold 0 + rootMargin
|
|
1199
|
+
// forces the "active" block to be the one just below the top of the
|
|
1200
|
+
// viewport — same pattern as Stripe Docs and Notion.
|
|
1201
|
+
const visible = new Map();
|
|
1202
|
+
_docScrollSpyObserver = new IntersectionObserver((entries) => {
|
|
1203
|
+
for (const entry of entries) {
|
|
1204
|
+
const id = entry.target.getAttribute('data-block-id');
|
|
1205
|
+
if (entry.isIntersecting) visible.set(id, entry.boundingClientRect.top);
|
|
1206
|
+
else visible.delete(id);
|
|
1207
|
+
}
|
|
1208
|
+
if (!visible.size) return;
|
|
1209
|
+
// Choose the entry with the smallest top — i.e. closest to the top of viewport.
|
|
1210
|
+
let topId = null, topY = Infinity;
|
|
1211
|
+
for (const [id, y] of visible) {
|
|
1212
|
+
if (y < topY) { topY = y; topId = id; }
|
|
1213
|
+
}
|
|
1214
|
+
setActive(headingFor.get(topId) || null);
|
|
1215
|
+
}, {
|
|
1216
|
+
root: area,
|
|
1217
|
+
rootMargin: '-12px 0px -70% 0px', // active = block crossing the top 30% band
|
|
1218
|
+
threshold: 0,
|
|
1219
|
+
});
|
|
1220
|
+
blocks.forEach(b => _docScrollSpyObserver.observe(b));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
628
1223
|
function renderDocRenderedPane(doc) {
|
|
629
1224
|
let html = `<article class="cr-doc-article" data-doc-path="${escAttr(doc.path)}">`;
|
|
630
1225
|
for (const block of doc.blocks) {
|
|
@@ -812,9 +1407,13 @@ function renderLoading() {
|
|
|
812
1407
|
function renderNoChanges() {
|
|
813
1408
|
const area = document.getElementById('cr-diff-area');
|
|
814
1409
|
const tree = document.getElementById('cr-file-tree');
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
1410
|
+
const mainBranch = (crState._baseInfo && crState._baseInfo.mainBranch) || 'main';
|
|
1411
|
+
let message;
|
|
1412
|
+
if (crState.baseRef === '--vs-main') message = `This branch has no changes vs ${mainBranch}`;
|
|
1413
|
+
else if (crState.baseRef === '--uncommitted') message = 'Nothing uncommitted — working tree and index are clean';
|
|
1414
|
+
else if (crState.baseRef === '--staged') message = 'No staged changes';
|
|
1415
|
+
else if (crState.baseRef) message = `Commit ${crState.baseRef.slice(0,7)} has no file changes`;
|
|
1416
|
+
else message = 'Working tree is clean against HEAD';
|
|
818
1417
|
if (area) area.innerHTML = `<div class="cr-no-changes">
|
|
819
1418
|
<div class="cr-no-changes-icon">✔</div>
|
|
820
1419
|
<div class="cr-no-changes-text">No changes detected</div>
|
|
@@ -846,8 +1445,12 @@ function renderFileTree() {
|
|
|
846
1445
|
const fExt = (f.path.match(/\.[^.]+$/) || [''])[0].toLowerCase();
|
|
847
1446
|
const isImage = f.isBinary && imgExts.includes(fExt);
|
|
848
1447
|
const icon = f.isNew ? '➕' : f.isDeleted ? '➖' : isImage ? '🖼' : f.isBinary ? '📦' : '📄';
|
|
849
|
-
|
|
1448
|
+
const viewed = crState.viewedFiles.has(f.path);
|
|
1449
|
+
return `<div class="cr-file-item ${isActive ? 'active' : ''} ${commentCount ? 'has-comments' : ''} ${viewed ? 'viewed' : ''}"
|
|
850
1450
|
onclick="CR.selectFile('${escAttr(f.path)}')">
|
|
1451
|
+
<span class="cr-file-viewed ${viewed ? 'on' : ''}" role="checkbox" aria-checked="${viewed}"
|
|
1452
|
+
title="${viewed ? 'Mark as not viewed' : 'Mark as viewed'} (v)"
|
|
1453
|
+
onclick="CR.toggleViewed('${escAttr(f.path)}', event)">${viewed ? '✔' : ''}</span>
|
|
851
1454
|
<span class="cr-file-icon">${icon}</span>
|
|
852
1455
|
<span class="cr-file-name" title="${escHtml(f.path)}">${escHtml(f.path)}</span>
|
|
853
1456
|
<span class="cr-file-stats">
|
|
@@ -857,6 +1460,7 @@ function renderFileTree() {
|
|
|
857
1460
|
${commentCount ? `<span class="cr-file-badge">${commentCount}</span>` : ''}
|
|
858
1461
|
</div>`;
|
|
859
1462
|
}).join('');
|
|
1463
|
+
_renderReviewProgress();
|
|
860
1464
|
}
|
|
861
1465
|
|
|
862
1466
|
CR.selectFile = function(filePath) {
|
|
@@ -905,6 +1509,151 @@ function _renderCommitSummaryCard(c) {
|
|
|
905
1509
|
return html;
|
|
906
1510
|
}
|
|
907
1511
|
|
|
1512
|
+
// Comment threads anchored exactly at (file, lineNum, side). Shared by unified + split.
|
|
1513
|
+
function _commentThreadRows(file, lineNum, side) {
|
|
1514
|
+
if (!lineNum) return '';
|
|
1515
|
+
const threadComments = crState.comments.filter(c =>
|
|
1516
|
+
c.file_path === file.path && c.side === side && (c.line_end || c.line_start) === lineNum
|
|
1517
|
+
);
|
|
1518
|
+
return threadComments.length ? renderCommentThread(threadComments, file.path, lineNum, side) : '';
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// An "expand hidden context" row. from/to are new-side line numbers; oldDelta maps a new
|
|
1522
|
+
// line number to its old number (oldNum = newNum + oldDelta) in the unchanged gap.
|
|
1523
|
+
function _expandRow(file, from, to, oldDelta) {
|
|
1524
|
+
const count = to > 0 ? (to - from + 1) : 0;
|
|
1525
|
+
if (to > 0 && count <= 0) return '';
|
|
1526
|
+
const label = count > 0 ? `Expand ${count} line${count > 1 ? 's' : ''}` : 'Expand remaining lines';
|
|
1527
|
+
return `<tr class="cr-diff-expand-row"><td colspan="4">
|
|
1528
|
+
<button class="cr-expand-btn" onclick="CR.expandContext(this, '${escAttr(file.path)}', ${from}, ${to}, ${oldDelta})">⋯ ${label}</button>
|
|
1529
|
+
</td></tr>`;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Unified diff table for one file (old + new gutters, single content column).
|
|
1533
|
+
function _renderFileDiffUnified(file) {
|
|
1534
|
+
let html = '<table class="cr-diff-table" data-file-path="' + escAttr(file.path) + '"><tbody>';
|
|
1535
|
+
for (let hi = 0; hi < file.hunks.length; hi++) {
|
|
1536
|
+
const hunk = file.hunks[hi];
|
|
1537
|
+
// Expand row for the gap before this hunk (leading context, or between hunks).
|
|
1538
|
+
if (!file.isNew && !file.isDeleted) {
|
|
1539
|
+
const oldDelta = hunk.oldStart - hunk.newStart;
|
|
1540
|
+
if (hi === 0) {
|
|
1541
|
+
if (hunk.newStart > 1) html += _expandRow(file, 1, hunk.newStart - 1, oldDelta);
|
|
1542
|
+
} else {
|
|
1543
|
+
const prev = file.hunks[hi - 1];
|
|
1544
|
+
const gapStart = prev.newStart + prev.newLines;
|
|
1545
|
+
const gapEnd = hunk.newStart - 1;
|
|
1546
|
+
if (gapEnd >= gapStart) html += _expandRow(file, gapStart, gapEnd, oldDelta);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
html += `<tr class="cr-diff-hunk-header"><td colspan="4">@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ ${escHtml(hunk.header)}</td></tr>`;
|
|
1550
|
+
for (const line of hunk.lines) {
|
|
1551
|
+
const cls = line.type === 'add' ? 'add' : line.type === 'del' ? 'del' : '';
|
|
1552
|
+
const prefix = line.type === 'add' ? '+' : line.type === 'del' ? '-' : ' ';
|
|
1553
|
+
const oldNum = line.oldNum != null ? line.oldNum : '';
|
|
1554
|
+
const newNum = line.newNum != null ? line.newNum : '';
|
|
1555
|
+
const side = line.type === 'del' ? 'old' : 'new';
|
|
1556
|
+
const lineNum = newNum || oldNum;
|
|
1557
|
+
const lineId = `${file.path}:${side}:${lineNum}`;
|
|
1558
|
+
html += `<tr class="cr-diff-line ${cls}" data-line-id="${escAttr(lineId)}" data-file="${escAttr(file.path)}" data-line="${lineNum}" data-side="${side}">
|
|
1559
|
+
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(file.path)}', ${oldNum || 'null'}, '${side}')">${oldNum}</td>
|
|
1560
|
+
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(file.path)}', ${newNum || 'null'}, 'new')">${newNum}</td>
|
|
1561
|
+
<td class="cr-line-prefix">${prefix}</td>
|
|
1562
|
+
<td class="cr-line-content">${escHtml(line.content)}</td>
|
|
1563
|
+
</tr>`;
|
|
1564
|
+
html += _commentThreadRows(file, lineNum, side);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
// Trailing expand row (context after the last hunk, to end of file).
|
|
1568
|
+
if (!file.isNew && !file.isDeleted && file.hunks.length) {
|
|
1569
|
+
const last = file.hunks[file.hunks.length - 1];
|
|
1570
|
+
const lastNewEnd = last.newStart + last.newLines - 1;
|
|
1571
|
+
const oldDelta = (last.oldStart + last.oldLines - 1) - lastNewEnd;
|
|
1572
|
+
html += _expandRow(file, lastNewEnd + 1, 0, oldDelta);
|
|
1573
|
+
}
|
|
1574
|
+
return html + '</tbody></table>';
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Fetch hidden context lines and splice them in where the "Expand" row was clicked.
|
|
1578
|
+
CR.expandContext = async function(btn, filePath, from, to, oldDelta) {
|
|
1579
|
+
if (!btn || btn._loading) return;
|
|
1580
|
+
btn._loading = true;
|
|
1581
|
+
const row = btn.closest('tr');
|
|
1582
|
+
const original = btn.innerHTML;
|
|
1583
|
+
btn.innerHTML = '⋯ Loading…';
|
|
1584
|
+
try {
|
|
1585
|
+
const q = new URLSearchParams({ project: crState.projectPath, path: filePath, from: String(from) });
|
|
1586
|
+
if (to > 0) q.set('to', String(to));
|
|
1587
|
+
if (crState.baseRef) q.set('ref', crState.baseRef);
|
|
1588
|
+
const data = await api('/reviews/file-lines?' + q.toString());
|
|
1589
|
+
const lines = data.lines || [];
|
|
1590
|
+
if (!lines.length) { row.remove(); return; }
|
|
1591
|
+
const rowsHtml = lines.map(l => {
|
|
1592
|
+
const newNum = l.num;
|
|
1593
|
+
const oldNum = newNum + oldDelta;
|
|
1594
|
+
const lineId = `${filePath}:new:${newNum}`;
|
|
1595
|
+
return `<tr class="cr-diff-line" data-line-id="${escAttr(lineId)}" data-file="${escAttr(filePath)}" data-line="${newNum}" data-side="new">
|
|
1596
|
+
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(filePath)}', ${oldNum}, 'old')">${oldNum}</td>
|
|
1597
|
+
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(filePath)}', ${newNum}, 'new')">${newNum}</td>
|
|
1598
|
+
<td class="cr-line-prefix"> </td>
|
|
1599
|
+
<td class="cr-line-content">${escHtml(l.text)}</td>
|
|
1600
|
+
</tr>`;
|
|
1601
|
+
}).join('');
|
|
1602
|
+
row.insertAdjacentHTML('beforebegin', rowsHtml);
|
|
1603
|
+
row.remove();
|
|
1604
|
+
} catch (e) {
|
|
1605
|
+
btn.innerHTML = original;
|
|
1606
|
+
btn._loading = false;
|
|
1607
|
+
if (typeof window.toast === 'function') window.toast('Could not expand context: ' + e.message, { type: 'error' });
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
// Align hunk lines into side-by-side rows: context lines pair 1:1; runs of deletions and
|
|
1612
|
+
// additions are paired index-wise (extra rows on one side get a blank cell opposite).
|
|
1613
|
+
function _alignHunkRows(lines) {
|
|
1614
|
+
const rows = [];
|
|
1615
|
+
let dels = [], adds = [];
|
|
1616
|
+
const flush = () => {
|
|
1617
|
+
const n = Math.max(dels.length, adds.length);
|
|
1618
|
+
for (let i = 0; i < n; i++) rows.push({ left: dels[i] || null, right: adds[i] || null });
|
|
1619
|
+
dels = []; adds = [];
|
|
1620
|
+
};
|
|
1621
|
+
for (const line of lines) {
|
|
1622
|
+
if (line.type === 'del') dels.push(line);
|
|
1623
|
+
else if (line.type === 'add') adds.push(line);
|
|
1624
|
+
else { flush(); rows.push({ left: line, right: line, ctx: true }); }
|
|
1625
|
+
}
|
|
1626
|
+
flush();
|
|
1627
|
+
return rows;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Side-by-side diff table for one file (old | new columns).
|
|
1631
|
+
function _renderFileDiffSplit(file) {
|
|
1632
|
+
const fp = escAttr(file.path);
|
|
1633
|
+
const cell = (line, sideNum, sideName, sideCls) => {
|
|
1634
|
+
if (!line) return `<td class="cr-line-num"></td><td class="cr-line-content cr-split-empty"></td>`;
|
|
1635
|
+
const num = line[sideNum] != null ? line[sideNum] : '';
|
|
1636
|
+
return `<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${fp}', ${num || 'null'}, '${sideName}')">${num}</td>
|
|
1637
|
+
<td class="cr-line-content ${sideCls}">${escHtml(line.content)}</td>`;
|
|
1638
|
+
};
|
|
1639
|
+
let html = '<table class="cr-diff-table split"><tbody>';
|
|
1640
|
+
for (const hunk of file.hunks) {
|
|
1641
|
+
html += `<tr class="cr-diff-hunk-header"><td colspan="4">@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ ${escHtml(hunk.header)}</td></tr>`;
|
|
1642
|
+
for (const row of _alignHunkRows(hunk.lines)) {
|
|
1643
|
+
const leftCls = row.ctx ? '' : (row.left ? 'del' : '');
|
|
1644
|
+
const rightCls = row.ctx ? '' : (row.right ? 'add' : '');
|
|
1645
|
+
html += `<tr class="cr-diff-line-split">
|
|
1646
|
+
${cell(row.left, 'oldNum', 'old', leftCls)}
|
|
1647
|
+
${cell(row.right, 'newNum', 'new', rightCls)}
|
|
1648
|
+
</tr>`;
|
|
1649
|
+
// Comment threads: anchor under the new side when present, else the old side.
|
|
1650
|
+
if (row.right && row.right.newNum != null) html += _commentThreadRows(file, row.right.newNum, 'new');
|
|
1651
|
+
else if (row.left && row.left.oldNum != null) html += _commentThreadRows(file, row.left.oldNum, 'old');
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return html + '</tbody></table>';
|
|
1655
|
+
}
|
|
1656
|
+
|
|
908
1657
|
function renderDiff() {
|
|
909
1658
|
const area = document.getElementById('cr-diff-area');
|
|
910
1659
|
if (!area) return;
|
|
@@ -954,9 +1703,13 @@ function renderDiff() {
|
|
|
954
1703
|
for (const file of crState.files) {
|
|
955
1704
|
const fileId = 'cr-file-' + escAttr(file.path);
|
|
956
1705
|
|
|
957
|
-
// Sticky file header
|
|
958
|
-
|
|
1706
|
+
// Sticky file header — viewed files collapse to just this header (click to expand).
|
|
1707
|
+
const viewed = crState.viewedFiles.has(file.path);
|
|
1708
|
+
html += `<div class="cr-diff-file-section ${viewed ? 'viewed' : ''}" id="${fileId}" data-file-path="${escAttr(file.path)}">`;
|
|
959
1709
|
html += `<div class="cr-diff-file-header sticky">
|
|
1710
|
+
<span class="cr-diff-file-viewed ${viewed ? 'on' : ''}" role="checkbox" aria-checked="${viewed}"
|
|
1711
|
+
title="${viewed ? 'Mark as not viewed' : 'Mark as viewed'} (v)"
|
|
1712
|
+
onclick="CR.toggleViewed('${escAttr(file.path)}', event)">${viewed ? '✔ Viewed' : 'Viewed'}</span>
|
|
960
1713
|
<span class="cr-diff-file-path">${escHtml(file.path)}</span>
|
|
961
1714
|
<span class="cr-file-stats" style="font-size:12px">
|
|
962
1715
|
<span class="cr-file-add">+${file.additions}</span>
|
|
@@ -984,41 +1737,8 @@ function renderDiff() {
|
|
|
984
1737
|
continue;
|
|
985
1738
|
}
|
|
986
1739
|
|
|
987
|
-
// Diff table
|
|
988
|
-
html += '
|
|
989
|
-
|
|
990
|
-
for (const hunk of file.hunks) {
|
|
991
|
-
html += `<tr class="cr-diff-hunk-header"><td colspan="4">@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ ${escHtml(hunk.header)}</td></tr>`;
|
|
992
|
-
|
|
993
|
-
for (let i = 0; i < hunk.lines.length; i++) {
|
|
994
|
-
const line = hunk.lines[i];
|
|
995
|
-
const cls = line.type === 'add' ? 'add' : line.type === 'del' ? 'del' : '';
|
|
996
|
-
const prefix = line.type === 'add' ? '+' : line.type === 'del' ? '-' : ' ';
|
|
997
|
-
const oldNum = line.oldNum != null ? line.oldNum : '';
|
|
998
|
-
const newNum = line.newNum != null ? line.newNum : '';
|
|
999
|
-
const lineId = `${file.path}:${line.type === 'del' ? 'old' : 'new'}:${newNum || oldNum}`;
|
|
1000
|
-
|
|
1001
|
-
html += `<tr class="cr-diff-line ${cls}" data-line-id="${escAttr(lineId)}" data-file="${escAttr(file.path)}" data-line="${newNum || oldNum}" data-side="${line.type === 'del' ? 'old' : 'new'}">
|
|
1002
|
-
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(file.path)}', ${oldNum || 'null'}, '${line.type === 'del' ? 'old' : 'new'}')">${oldNum}</td>
|
|
1003
|
-
<td class="cr-line-num" onmousedown="CR.onLineClick(event, '${escAttr(file.path)}', ${newNum || 'null'}, 'new')">${newNum}</td>
|
|
1004
|
-
<td class="cr-line-prefix">${prefix}</td>
|
|
1005
|
-
<td class="cr-line-content">${escHtml(line.content)}</td>
|
|
1006
|
-
</tr>`;
|
|
1007
|
-
|
|
1008
|
-
// Insert comment threads after this line
|
|
1009
|
-
const lineNum = newNum || oldNum;
|
|
1010
|
-
const side = line.type === 'del' ? 'old' : 'new';
|
|
1011
|
-
const lineComments = crState.comments.filter(c =>
|
|
1012
|
-
c.file_path === file.path && c.line_start <= lineNum && (c.line_end || c.line_start) >= lineNum && c.side === side
|
|
1013
|
-
);
|
|
1014
|
-
const threadComments = lineComments.filter(c => (c.line_end || c.line_start) === lineNum);
|
|
1015
|
-
if (threadComments.length > 0) {
|
|
1016
|
-
html += renderCommentThread(threadComments, file.path, lineNum, side);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
html += '</tbody></table>';
|
|
1740
|
+
// Diff table — unified or side-by-side depending on crState.diffMode
|
|
1741
|
+
html += crState.diffMode === 'split' ? _renderFileDiffSplit(file) : _renderFileDiffUnified(file);
|
|
1022
1742
|
html += '</div>'; // close file section
|
|
1023
1743
|
}
|
|
1024
1744
|
|
|
@@ -1118,8 +1838,14 @@ function renderFooter() {
|
|
|
1118
1838
|
? `${openComments.length} ${commentWord} (${parts.join(', ')}) across ${fileCount} files`
|
|
1119
1839
|
: `${fileCount} files changed, no comments yet`);
|
|
1120
1840
|
|
|
1841
|
+
const resolveAllBtn = openComments.length > 1
|
|
1842
|
+
? `<button class="cr-btn cr-resolve-all" onclick="CR.resolveAllComments()" title="Resolve all open comments">Resolve all (${openComments.length})</button>`
|
|
1843
|
+
: '';
|
|
1844
|
+
|
|
1121
1845
|
footer.innerHTML = `
|
|
1122
1846
|
<div class="cr-footer-summary">${summary}</div>
|
|
1847
|
+
<div class="cr-progress" id="cr-progress"></div>
|
|
1848
|
+
${resolveAllBtn}
|
|
1123
1849
|
<div class="send-dropdown-wrap" id="cr-footer-send-wrap">
|
|
1124
1850
|
<button class="cr-btn primary btn-main cr-footer-submit" onclick="CR.submitReview()" ${openComments.length === 0 ? 'disabled' : ''}>Send</button>
|
|
1125
1851
|
<button class="cr-btn primary btn-caret" onclick="CR.toggleSendDropdown(event)" ${openComments.length === 0 ? 'disabled' : ''}>▼</button>
|
|
@@ -1130,6 +1856,8 @@ function renderFooter() {
|
|
|
1130
1856
|
// Also update submit button in header
|
|
1131
1857
|
const submitBtn = document.getElementById('cr-submit-btn');
|
|
1132
1858
|
if (submitBtn) submitBtn.disabled = openComments.length === 0;
|
|
1859
|
+
|
|
1860
|
+
_renderReviewProgress();
|
|
1133
1861
|
}
|
|
1134
1862
|
|
|
1135
1863
|
// --- Line selection (click-drag or shift-click) → add comment ---
|
|
@@ -1297,6 +2025,18 @@ CR.resolveComment = async function(commentId) {
|
|
|
1297
2025
|
}
|
|
1298
2026
|
};
|
|
1299
2027
|
|
|
2028
|
+
CR.resolveAllComments = async function() {
|
|
2029
|
+
const open = crState.comments.filter(c => c.status === 'open');
|
|
2030
|
+
if (!open.length) return;
|
|
2031
|
+
if (typeof window.confirm === 'function' && !window.confirm(`Resolve all ${open.length} open comment${open.length > 1 ? 's' : ''}?`)) return;
|
|
2032
|
+
// Optimistic local update + render once; persist each in parallel.
|
|
2033
|
+
open.forEach(c => { c.status = 'resolved'; });
|
|
2034
|
+
renderCurrentReviewSurface();
|
|
2035
|
+
const results = await Promise.allSettled(open.map(c => api(`/review-comments/${c.id}`, 'PUT', { status: 'resolved' })));
|
|
2036
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
2037
|
+
if (failed && typeof window.toast === 'function') window.toast(`${failed} comment(s) failed to resolve — reload to retry`, { type: 'error' });
|
|
2038
|
+
};
|
|
2039
|
+
|
|
1300
2040
|
CR.reopenComment = async function(commentId) {
|
|
1301
2041
|
try {
|
|
1302
2042
|
await api(`/review-comments/${commentId}`, 'PUT', { status: 'open' });
|
|
@@ -1669,11 +2409,23 @@ CR.handleFilesChanged = function(msg) {
|
|
|
1669
2409
|
for (const count of projectChanges.values()) total += count;
|
|
1670
2410
|
updateBadge(total);
|
|
1671
2411
|
|
|
1672
|
-
//
|
|
1673
|
-
|
|
2412
|
+
// Keep the chooser responsive: update the visible cached row immediately and
|
|
2413
|
+
// let the server's bounded background scan refresh exact branch/file metadata.
|
|
2414
|
+
if (!crState.reviewId && crState._view === 'projects') {
|
|
1674
2415
|
const panel = document.getElementById('codereview-panel');
|
|
1675
2416
|
if (panel && panel.classList.contains('active')) {
|
|
1676
|
-
|
|
2417
|
+
const projects = projectChooserState.projects || [];
|
|
2418
|
+
const project = projects.find(p => p && p.path === msg.project);
|
|
2419
|
+
if (project) {
|
|
2420
|
+
project.fileCount = Number(msg.fileCount || 0);
|
|
2421
|
+
if (Array.isArray(msg.files)) {
|
|
2422
|
+
project.files = msg.files.map(f => typeof f === 'string'
|
|
2423
|
+
? { path: f, additions: 0, deletions: 0 }
|
|
2424
|
+
: f).filter(Boolean);
|
|
2425
|
+
}
|
|
2426
|
+
updateReviewProjectResults();
|
|
2427
|
+
}
|
|
2428
|
+
scheduleProjectChooserRefresh(1800);
|
|
1677
2429
|
}
|
|
1678
2430
|
}
|
|
1679
2431
|
|
|
@@ -1688,6 +2440,15 @@ CR.handleFilesChanged = function(msg) {
|
|
|
1688
2440
|
}
|
|
1689
2441
|
};
|
|
1690
2442
|
|
|
2443
|
+
CR.handleReviewProjectsUpdated = function(msg) {
|
|
2444
|
+
if (crState._view !== 'projects') return;
|
|
2445
|
+
const panel = document.getElementById('codereview-panel');
|
|
2446
|
+
if (!panel || !panel.classList.contains('active')) return;
|
|
2447
|
+
projectChooserState.scan = msg && msg.scan ? msg.scan : projectChooserState.scan;
|
|
2448
|
+
updateReviewProjectResults();
|
|
2449
|
+
scheduleProjectChooserRefresh(msg && msg.scan && msg.scan.running ? 1200 : 150);
|
|
2450
|
+
};
|
|
2451
|
+
|
|
1691
2452
|
function updateBadge(count) {
|
|
1692
2453
|
let badge = document.getElementById('cr-nav-badge');
|
|
1693
2454
|
const navBtn = document.querySelector('[data-nav="codereview"]');
|
|
@@ -1878,6 +2639,9 @@ document.addEventListener('keydown', function(e) {
|
|
|
1878
2639
|
} else if ((e.key === 'k' || e.key === '[') && curIdx > 0) {
|
|
1879
2640
|
e.preventDefault();
|
|
1880
2641
|
CR.selectFile(crState.files[curIdx - 1].path);
|
|
2642
|
+
} else if (e.key === 'v' && crState.reviewType === 'code') {
|
|
2643
|
+
e.preventDefault();
|
|
2644
|
+
CR.toggleViewed(crState.activeFile);
|
|
1881
2645
|
}
|
|
1882
2646
|
});
|
|
1883
2647
|
|
|
@@ -1934,7 +2698,11 @@ window.crState = crState;
|
|
|
1934
2698
|
if (window._ctmState?.pendingDocumentReview) {
|
|
1935
2699
|
const pending = window._ctmState.pendingDocumentReview;
|
|
1936
2700
|
delete window._ctmState.pendingDocumentReview;
|
|
1937
|
-
setTimeout(() => CR.openDocumentReview(pending.path, pending.line || 1
|
|
2701
|
+
setTimeout(() => CR.openDocumentReview(pending.path, pending.line || 1, {
|
|
2702
|
+
cwd: pending.cwd || '',
|
|
2703
|
+
sessionId: pending.sessionId || '',
|
|
2704
|
+
fromHash: !!pending.fromHash,
|
|
2705
|
+
}), 0);
|
|
1938
2706
|
}
|
|
1939
2707
|
|
|
1940
2708
|
})();
|