@ripla/godd-mcp 1.0.4-canary.9 → 1.0.4
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 +89 -94
- package/dist/godd.cjs +349 -233
- package/dist/godd.js +2193 -838
- package/dist/index.js +152 -122
- package/{notes-api → notes/api}/.env.example +6 -1
- package/{notes-api → notes/api}/README.md +6 -5
- package/{notes-api → notes/api}/app/main.py +29 -2
- package/{notes-api → notes/api}/app/routers/auth.py +6 -1
- package/{notes-api → notes/api}/app/routers/issues.py +157 -1
- package/{notes-api → notes/api}/app/routers/pr.py +2 -2
- package/{notes-api → notes/api}/app/services/github_app_token.py +32 -1
- package/{notes-api → notes/api}/app/services/github_issues.py +101 -11
- package/{notes-api → notes/api}/app/services/github_pr.py +6 -1
- package/{notes-api → notes/api}/tests/test_github_app_token.py +49 -1
- package/{notes-api → notes/api}/tests/test_github_service.py +42 -0
- package/notes/api/tests/test_health.py +52 -0
- package/{notes-api → notes/api}/tests/test_issues.py +216 -0
- package/{notes-api → notes/api}/tests/test_pr.py +24 -0
- package/{notes-api → notes/api}/tests/test_pr_chain.py +19 -0
- package/{notes-api → notes/api}/uv.lock +178 -2
- package/notes/app/DESIGN.md +273 -0
- package/{notes-app → notes/app}/README.md +4 -1
- package/notes/app/docker/Dockerfile.e2e +19 -0
- package/notes/app/docker/Dockerfile.prod +15 -0
- package/notes/app/docker/Dockerfile.test +17 -0
- package/notes/app/e2e/file-tree.spec.ts +26 -0
- package/notes/app/e2e/file-view.spec.ts +25 -0
- package/notes/app/e2e/fixtures.ts +28 -0
- package/{notes-app → notes/app}/e2e/global-setup.ts +5 -7
- package/notes/app/e2e/login.spec.ts +25 -0
- package/notes/app/e2e/pages/login-page.ts +29 -0
- package/notes/app/e2e/pages/main-page.ts +29 -0
- package/notes/app/e2e/panel-resize-handle.spec.ts +213 -0
- package/{notes-app → notes/app}/eslint.config.js +9 -0
- package/{notes-app → notes/app}/package.json +0 -3
- package/{notes-app → notes/app}/pnpm-workspace.yaml +1 -0
- package/notes/app/src/components/CommentPanel.test.tsx +194 -0
- package/{notes-app → notes/app}/src/components/CommentPanel.tsx +119 -10
- package/notes/app/src/components/CommentSelectionPopover.tsx +26 -0
- package/notes/app/src/components/EditToolbar.test.tsx +102 -0
- package/{notes-app → notes/app}/src/components/EditToolbar.tsx +5 -54
- package/notes/app/src/components/FileInfoPanel.branch-tab.test.tsx +250 -0
- package/notes/app/src/components/FileInfoPanel.history-tab.test.tsx +146 -0
- package/notes/app/src/components/FileInfoPanel.test.tsx +101 -0
- package/{notes-app → notes/app}/src/components/FileInfoPanel.tsx +135 -20
- package/notes/app/src/components/IssueCommentThread.test.tsx +93 -0
- package/notes/app/src/components/IssueCommentThread.tsx +156 -0
- package/notes/app/src/components/IssueDetailView.test.tsx +1118 -0
- package/notes/app/src/components/IssueDetailView.tsx +476 -0
- package/notes/app/src/components/IssueList.test.tsx +545 -0
- package/notes/app/src/components/IssueList.tsx +374 -0
- package/{notes-app → notes/app}/src/components/LabelSelector.test.tsx +94 -8
- package/notes/app/src/components/LabelSelector.tsx +452 -0
- package/{notes-app → notes/app}/src/components/MarkdownEditor.tsx +140 -2
- package/notes/app/src/components/PanelResizeHandle.test.tsx +481 -0
- package/notes/app/src/components/PanelResizeHandle.tsx +96 -0
- package/{notes-app → notes/app}/src/hooks/useEditSession.test.ts +171 -23
- package/{notes-app → notes/app}/src/hooks/useEditSession.ts +49 -62
- package/notes/app/src/hooks/useMdUpLayout.test.ts +10 -0
- package/notes/app/src/hooks/useMdUpLayout.ts +27 -0
- package/notes/app/src/hooks/usePeriodicSync.test.ts +244 -0
- package/notes/app/src/hooks/usePeriodicSync.ts +83 -0
- package/notes/app/src/hooks/useResizablePanelWidth.test.ts +251 -0
- package/notes/app/src/hooks/useResizablePanelWidth.ts +167 -0
- package/{notes-app → notes/app}/src/lib/api.test.ts +81 -1
- package/{notes-app → notes/app}/src/lib/api.ts +98 -4
- package/notes/app/src/lib/commentAnchor.test.ts +22 -0
- package/notes/app/src/lib/commentAnchor.ts +28 -0
- package/notes/app/src/lib/commentHighlight.test.ts +110 -0
- package/notes/app/src/lib/commentHighlight.ts +44 -0
- package/notes/app/src/lib/github-sync.test.ts +126 -0
- package/notes/app/src/lib/github-sync.ts +89 -0
- package/notes/app/src/lib/issue-list-utils.test.ts +275 -0
- package/notes/app/src/lib/issue-list-utils.ts +135 -0
- package/notes/app/src/lib/issue-mobile-layout.test.ts +17 -0
- package/notes/app/src/lib/issue-mobile-layout.ts +20 -0
- package/notes/app/src/lib/issue-url-sync.test.ts +73 -0
- package/notes/app/src/lib/issue-url-sync.ts +43 -0
- package/notes/app/src/lib/main-page-default-view.test.ts +37 -0
- package/notes/app/src/lib/main-page-default-view.ts +15 -0
- package/notes/app/src/lib/resizable-panel.test.ts +199 -0
- package/notes/app/src/lib/resizable-panel.ts +136 -0
- package/notes/app/src/pages/LoginPage.test.tsx +17 -0
- package/notes/app/src/pages/MainPage.branch-modal.test.tsx +226 -0
- package/notes/app/src/pages/MainPage.issue-sync.test.tsx +145 -0
- package/notes/app/src/pages/MainPage.sidebar.test.tsx +361 -0
- package/{notes-app → notes/app}/src/pages/MainPage.tsx +406 -61
- package/{notes-app → notes/app}/src/pages/SettingsPage.test.tsx +32 -23
- package/{notes-app → notes/app}/src/pages/UserManagementPage.test.tsx +8 -18
- package/notes/app/src/test/mocks.ts +156 -0
- package/notes/app/src/test/render.tsx +27 -0
- package/package.json +3 -5
- package/notes-api/tests/test_health.py +0 -11
- package/notes-app/docker/Dockerfile.e2e +0 -14
- package/notes-app/docker/Dockerfile.prod +0 -12
- package/notes-app/docker/Dockerfile.test +0 -12
- package/notes-app/e2e/file-tree.spec.ts +0 -29
- package/notes-app/e2e/file-view.spec.ts +0 -24
- package/notes-app/e2e/login.spec.ts +0 -32
- package/notes-app/src/components/IssueDetailView.test.tsx +0 -201
- package/notes-app/src/components/IssueDetailView.tsx +0 -318
- package/notes-app/src/components/IssueList.test.tsx +0 -93
- package/notes-app/src/components/IssueList.tsx +0 -169
- package/notes-app/src/components/LabelSelector.tsx +0 -276
- package/notes-app/src/pages/LoginPage.test.tsx +0 -29
- package/stacks/django-vue.yaml +0 -33
- package/stacks/fastapi-react.yaml +0 -160
- package/stacks/flutter-firebase.yaml +0 -58
- package/stacks/nextjs-prisma.yaml +0 -33
- package/stacks/schema.json +0 -144
- package/templates/agents/architect.hbs +0 -183
- package/templates/agents/auto-documenter.hbs +0 -36
- package/templates/agents/backend-developer.hbs +0 -175
- package/templates/agents/code-reviewer.hbs +0 -29
- package/templates/agents/database-developer.hbs +0 -52
- package/templates/agents/debug-specialist.hbs +0 -39
- package/templates/agents/environment-setup.hbs +0 -41
- package/templates/agents/frontend-developer.hbs +0 -45
- package/templates/agents/integration-qa.hbs +0 -26
- package/templates/agents/performance-qa.hbs +0 -24
- package/templates/agents/pr-writer.hbs +0 -53
- package/templates/agents/quality-lead.hbs +0 -26
- package/templates/agents/requirements-analyst.hbs +0 -26
- package/templates/agents/security-analyst.hbs +0 -21
- package/templates/agents/security-reviewer.hbs +0 -20
- package/templates/agents/ssot-updater.hbs +0 -45
- package/templates/agents/technical-writer.hbs +0 -28
- package/templates/agents/unit-test-engineer.hbs +0 -19
- package/templates/agents/user-clone.hbs +0 -49
- package/templates/github-actions/aws/deploy-notes.yml.hbs +0 -117
- package/templates/github-actions/azure/deploy-notes.yml.hbs +0 -78
- package/templates/github-actions/gcp/deploy-notes.yml.hbs +0 -76
- package/templates/github-actions/vercel-railway/deploy-notes.yml.hbs +0 -49
- package/templates/mindsets/agent-stdio.hbs +0 -51
- package/templates/mindsets/dart.hbs +0 -39
- package/templates/mindsets/ddd-clean-architecture.hbs +0 -52
- package/templates/mindsets/electron.hbs +0 -41
- package/templates/mindsets/fastapi.hbs +0 -40
- package/templates/mindsets/flutter.hbs +0 -56
- package/templates/mindsets/kotlin.hbs +0 -46
- package/templates/mindsets/postgresql.hbs +0 -39
- package/templates/mindsets/python.hbs +0 -39
- package/templates/mindsets/react.hbs +0 -51
- package/templates/mindsets/swift.hbs +0 -46
- package/templates/mindsets/terraform.hbs +0 -53
- package/templates/mindsets/typescript.hbs +0 -34
- package/templates/mindsets/vite.hbs +0 -37
- package/templates/notes-compose.yml +0 -82
- package/templates/partials/cogito-geo.hbs +0 -84
- package/templates/partials/cogito-godd.hbs +0 -22
- package/templates/partials/cogito-grammar.hbs +0 -105
- package/templates/partials/cogito-particle.hbs +0 -76
- package/templates/partials/cogito-root-ai.hbs +0 -79
- package/templates/partials/cogito-root-arch.hbs +0 -75
- package/templates/partials/cogito-root-core.hbs +0 -75
- package/templates/partials/cogito-root-daily.hbs +0 -58
- package/templates/partials/cogito-root-dev.hbs +0 -71
- package/templates/partials/cogito-root-devops.hbs +0 -71
- package/templates/partials/cogito-root-framework.hbs +0 -80
- package/templates/partials/cogito-root-lang.hbs +0 -74
- package/templates/partials/godd-core.hbs +0 -50
- package/templates/partials/project-context.hbs +0 -52
- package/templates/partials/ssot-header.hbs +0 -40
- package/templates/partials/ssot-rules.hbs +0 -30
- package/templates/prompts/adr.hbs +0 -39
- package/templates/prompts/agent-dev-workflow.hbs +0 -34
- package/templates/prompts/analyze-report.hbs +0 -36
- package/templates/prompts/auto-documentation.hbs +0 -37
- package/templates/prompts/check.hbs +0 -52
- package/templates/prompts/commit.hbs +0 -96
- package/templates/prompts/dev.hbs +0 -127
- package/templates/prompts/docs-init.hbs +0 -101
- package/templates/prompts/docs-update.hbs +0 -51
- package/templates/prompts/git-workflow.hbs +0 -125
- package/templates/prompts/impact-analysis.hbs +0 -43
- package/templates/prompts/install.hbs +0 -40
- package/templates/prompts/new-branch.hbs +0 -61
- package/templates/prompts/notes-deploy.hbs +0 -138
- package/templates/prompts/pr-analyze.hbs +0 -44
- package/templates/prompts/pr-create.hbs +0 -125
- package/templates/prompts/push-execute.hbs +0 -83
- package/templates/prompts/push.hbs +0 -24
- package/templates/prompts/release-notes.hbs +0 -38
- package/templates/prompts/requirements-to-business-flow.hbs +0 -29
- package/templates/prompts/requirements-to-tickets.hbs +0 -26
- package/templates/prompts/review.hbs +0 -126
- package/templates/prompts/setup.hbs +0 -247
- package/templates/prompts/spec.hbs +0 -35
- package/templates/prompts/specification-to-tickets.hbs +0 -24
- package/templates/prompts/submit.hbs +0 -143
- package/templates/prompts/sync.hbs +0 -63
- package/templates/prompts/test-plan.hbs +0 -56
- package/templates/scripts/aws/terraform-backend-setup.sh.hbs +0 -149
- package/templates/terraform/aws/alb.tf.hbs +0 -58
- package/templates/terraform/aws/backend.tf.hbs +0 -10
- package/templates/terraform/aws/cloudfront.tf.hbs +0 -96
- package/templates/terraform/aws/ecr.tf.hbs +0 -30
- package/templates/terraform/aws/ecs.tf.hbs +0 -142
- package/templates/terraform/aws/iam.tf.hbs +0 -88
- package/templates/terraform/aws/local.tf.hbs +0 -34
- package/templates/terraform/aws/output.tf.hbs +0 -49
- package/templates/terraform/aws/provider.tf.hbs +0 -26
- package/templates/terraform/aws/rds.tf.hbs +0 -38
- package/templates/terraform/aws/s3.tf.hbs +0 -34
- package/templates/terraform/aws/secrets.tf.hbs +0 -53
- package/templates/terraform/aws/security_groups.tf.hbs +0 -81
- package/templates/terraform/aws/vpc.tf.hbs +0 -63
- package/templates/terraform/azure/backend.tf.hbs +0 -8
- package/templates/terraform/azure/iam.tf.hbs +0 -36
- package/templates/terraform/azure/main.tf.hbs +0 -203
- package/templates/terraform/azure/output.tf.hbs +0 -19
- package/templates/terraform/azure/provider.tf.hbs +0 -19
- package/templates/terraform/gcp/backend.tf.hbs +0 -6
- package/templates/terraform/gcp/iam.tf.hbs +0 -47
- package/templates/terraform/gcp/main.tf.hbs +0 -175
- package/templates/terraform/gcp/output.tf.hbs +0 -15
- package/templates/terraform/gcp/provider.tf.hbs +0 -19
- package/templates/terraform/vercel-railway/main.tf.hbs +0 -64
- package/templates/terraform/vercel-railway/output.tf.hbs +0 -11
- /package/{notes-api → notes/api}/.dockerignore +0 -0
- /package/{notes-api → notes/api}/alembic/README +0 -0
- /package/{notes-api → notes/api}/alembic/env.py +0 -0
- /package/{notes-api → notes/api}/alembic/script.py.mako +0 -0
- /package/{notes-api → notes/api}/alembic/versions/001_initial_notes_tables.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/002_add_performance_indexes.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/003_add_yjs_documents.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/004_add_comments.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/005_add_github_username.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/006_add_check_constraints.py +0 -0
- /package/{notes-api → notes/api}/alembic.ini +0 -0
- /package/{notes-api → notes/api}/app/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/config.py +0 -0
- /package/{notes-api → notes/api}/app/database.py +0 -0
- /package/{notes-api → notes/api}/app/http_client.py +0 -0
- /package/{notes-api → notes/api}/app/models/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/models/audit.py +0 -0
- /package/{notes-api → notes/api}/app/models/base.py +0 -0
- /package/{notes-api → notes/api}/app/models/collab.py +0 -0
- /package/{notes-api → notes/api}/app/models/comment.py +0 -0
- /package/{notes-api → notes/api}/app/models/session.py +0 -0
- /package/{notes-api → notes/api}/app/models/setting.py +0 -0
- /package/{notes-api → notes/api}/app/models/user.py +0 -0
- /package/{notes-api → notes/api}/app/path_validation.py +0 -0
- /package/{notes-api → notes/api}/app/routers/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/routers/collab.py +0 -0
- /package/{notes-api → notes/api}/app/routers/comments.py +0 -0
- /package/{notes-api → notes/api}/app/routers/files.py +0 -0
- /package/{notes-api → notes/api}/app/routers/settings.py +0 -0
- /package/{notes-api → notes/api}/app/routers/tree.py +0 -0
- /package/{notes-api → notes/api}/app/routers/upload.py +0 -0
- /package/{notes-api → notes/api}/app/routers/users.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/auth.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/user.py +0 -0
- /package/{notes-api → notes/api}/app/security.py +0 -0
- /package/{notes-api → notes/api}/app/services/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/services/audit.py +0 -0
- /package/{notes-api → notes/api}/app/services/business_date.py +0 -0
- /package/{notes-api → notes/api}/app/services/collab.py +0 -0
- /package/{notes-api → notes/api}/app/services/github.py +0 -0
- /package/{notes-api → notes/api}/app/services/pr_chain.py +0 -0
- /package/{notes-api → notes/api}/app/startup.py +0 -0
- /package/{notes-api → notes/api}/docker/Dockerfile +0 -0
- /package/{notes-api → notes/api}/docker/Dockerfile.test +0 -0
- /package/{notes-api → notes/api}/pyproject.toml +0 -0
- /package/{notes-api → notes/api}/tests/conftest.py +0 -0
- /package/{notes-api → notes/api}/tests/test_audit.py +0 -0
- /package/{notes-api → notes/api}/tests/test_auth.py +0 -0
- /package/{notes-api → notes/api}/tests/test_business_date.py +0 -0
- /package/{notes-api → notes/api}/tests/test_collab.py +0 -0
- /package/{notes-api → notes/api}/tests/test_comments.py +0 -0
- /package/{notes-api → notes/api}/tests/test_config_ssl.py +0 -0
- /package/{notes-api → notes/api}/tests/test_config_warnings.py +0 -0
- /package/{notes-api → notes/api}/tests/test_files.py +0 -0
- /package/{notes-api → notes/api}/tests/test_path_validation.py +0 -0
- /package/{notes-api → notes/api}/tests/test_security.py +0 -0
- /package/{notes-api → notes/api}/tests/test_settings.py +0 -0
- /package/{notes-api → notes/api}/tests/test_tree.py +0 -0
- /package/{notes-api → notes/api}/tests/test_upload.py +0 -0
- /package/{notes-api → notes/api}/tests/test_users.py +0 -0
- /package/{notes-app → notes/app}/.dockerignore +0 -0
- /package/{notes-app → notes/app}/index.html +0 -0
- /package/{notes-app → notes/app}/nginx.conf +0 -0
- /package/{notes-app → notes/app}/package-lock.json +0 -0
- /package/{notes-app → notes/app}/playwright.config.ts +0 -0
- /package/{notes-app → notes/app}/pnpm-lock.yaml +0 -0
- /package/{notes-app → notes/app}/src/App.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/BranchSelectorModal.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CodeBlockView.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CodeBlockView.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CollabStatus.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DiffPreview.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DrawioEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DrawioViewer.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ErrorBoundary.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ErrorBoundary.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContentView.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContextMenu.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContextMenu.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileDialogs.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileDialogs.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/Skeleton.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/TextEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ToastContainer.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/TreeNode.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/AuthContext.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/AuthContext.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/ToastContext.test.ts +0 -0
- /package/{notes-app → notes/app}/src/contexts/ToastContext.tsx +0 -0
- /package/{notes-app → notes/app}/src/hooks/useAutoSave.test.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useAutoSave.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useCollaboration.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useComments.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useYText.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/branch-diff.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/branch-diff.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/created-file.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/created-file.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/csv-utils.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/csv-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/draft-storage.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/draft-storage.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/mermaid-render.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/postLoginRedirect.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/postLoginRedirect.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/tree-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/user-colors.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/xlsx-utils.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/xlsx-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/main.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/LoginPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/MainPage.insertChildEntry.test.ts +0 -0
- /package/{notes-app → notes/app}/src/pages/SettingsPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/UserManagementPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/styles/globals.css +0 -0
- /package/{notes-app → notes/app}/src/types.ts +0 -0
- /package/{notes-app → notes/app}/src/vite-env.d.ts +0 -0
- /package/{notes-app → notes/app}/tsconfig.json +0 -0
- /package/{notes-app → notes/app}/tsconfig.node.json +0 -0
- /package/{notes-app → notes/app}/vite.config.js +0 -0
- /package/{notes-app → notes/app}/vite.config.ts +0 -0
- /package/{notes-app → notes/app}/vitest.config.ts +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useReducer, useEffect, useCallback } from "react";
|
|
1
|
+
import { useState, useReducer, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { X, RefreshCw, FileText, Clock, GitBranch, AlertCircle, Loader2, ExternalLink } from "lucide-react";
|
|
3
3
|
import {
|
|
4
4
|
fetchHistoryApi,
|
|
@@ -11,6 +11,10 @@ import type { CommitEntry, BranchStatus, SessionInfo } from "@/lib/api";
|
|
|
11
11
|
import type { CollabUser } from "@/hooks/useCollaboration";
|
|
12
12
|
import type { FileContent } from "@/types";
|
|
13
13
|
import { getFileExt } from "@/types";
|
|
14
|
+
import PanelResizeHandle from "@/components/PanelResizeHandle";
|
|
15
|
+
import { RESIZABLE_RIGHT_PANEL_ASIDE_CLASS, type PanelResizeProps } from "@/lib/resizable-panel";
|
|
16
|
+
import { usePeriodicSync } from "@/hooks/usePeriodicSync";
|
|
17
|
+
import { GITHUB_SYNC_POLL_INTERVAL_MS } from "@/lib/github-sync";
|
|
14
18
|
|
|
15
19
|
type Tab = "details" | "history" | "branch";
|
|
16
20
|
|
|
@@ -18,6 +22,8 @@ type Props = {
|
|
|
18
22
|
fileContent: FileContent;
|
|
19
23
|
collabUsers: CollabUser[];
|
|
20
24
|
onClose: () => void;
|
|
25
|
+
panelResize?: PanelResizeProps;
|
|
26
|
+
syncEnabled?: boolean;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
function TabButton({
|
|
@@ -116,20 +122,43 @@ function historyReducer(state: HistoryState, action: HistoryAction): HistoryStat
|
|
|
116
122
|
|
|
117
123
|
function HistoryTab({ filePath }: { filePath: string }) {
|
|
118
124
|
const [state, dispatch] = useReducer(historyReducer, { items: [], loading: false, error: null });
|
|
125
|
+
const requestSeq = useRef(0);
|
|
126
|
+
const mountedRef = useRef(true);
|
|
127
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
mountedRef.current = true;
|
|
131
|
+
return () => {
|
|
132
|
+
mountedRef.current = false;
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
119
135
|
|
|
120
136
|
const load = useCallback(() => {
|
|
137
|
+
// 進行中の取得をキャンセルし、後着の stale 応答で上書きされるのを防ぐ
|
|
138
|
+
abortRef.current?.abort();
|
|
139
|
+
const ac = new AbortController();
|
|
140
|
+
abortRef.current = ac;
|
|
141
|
+
const requestId = requestSeq.current + 1;
|
|
142
|
+
requestSeq.current = requestId;
|
|
143
|
+
|
|
121
144
|
dispatch({ type: "LOADING" });
|
|
122
|
-
fetchHistoryApi(filePath)
|
|
123
|
-
.then((data) =>
|
|
145
|
+
fetchHistoryApi(filePath, { signal: ac.signal })
|
|
146
|
+
.then((data) => {
|
|
147
|
+
if (!mountedRef.current || requestSeq.current !== requestId) return;
|
|
148
|
+
dispatch({ type: "SUCCESS", items: data });
|
|
149
|
+
})
|
|
124
150
|
.catch((err: unknown) => {
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
}
|
|
151
|
+
if (!mountedRef.current || requestSeq.current !== requestId) return;
|
|
152
|
+
if (isAbortError(err)) return;
|
|
153
|
+
dispatch({ type: "ERROR", message: getErrorMessage(err, "履歴の取得に失敗しました") });
|
|
128
154
|
});
|
|
129
155
|
}, [filePath]);
|
|
130
156
|
|
|
131
157
|
useEffect(() => {
|
|
132
158
|
load();
|
|
159
|
+
return () => {
|
|
160
|
+
abortRef.current?.abort();
|
|
161
|
+
};
|
|
133
162
|
}, [load]);
|
|
134
163
|
|
|
135
164
|
const { items: history, loading, error } = state;
|
|
@@ -152,7 +181,7 @@ function HistoryTab({ filePath }: { filePath: string }) {
|
|
|
152
181
|
</div>
|
|
153
182
|
<button
|
|
154
183
|
type="button"
|
|
155
|
-
onClick={load}
|
|
184
|
+
onClick={() => load()}
|
|
156
185
|
className="mt-2 text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
|
157
186
|
>
|
|
158
187
|
<RefreshCw className="w-3 h-3" />
|
|
@@ -199,26 +228,92 @@ function branchReducer(state: BranchState, action: BranchAction): BranchState {
|
|
|
199
228
|
}
|
|
200
229
|
}
|
|
201
230
|
|
|
202
|
-
function BranchTab() {
|
|
231
|
+
function BranchTab({ syncEnabled }: { syncEnabled: boolean }) {
|
|
203
232
|
const [state, dispatch] = useReducer(branchReducer, { status: null, sessions: [], loading: false, error: null });
|
|
233
|
+
// foreground(初回・明示的再取得)と background poll で seq を分離し、即時 background poll が
|
|
234
|
+
// in-flight の foreground SUCCESS を stale 扱いで破棄しないようにする(#934)
|
|
235
|
+
const loadRequestSeq = useRef(0);
|
|
236
|
+
const pollRequestSeq = useRef(0);
|
|
237
|
+
// freshness ゲート: 取得ごとに単調増加する世代を採番し、適用済み世代(appliedGen)以下の取得結果は
|
|
238
|
+
// データを上書きしない。これにより「遅い foreground load が、後から始まったより新しい background poll の
|
|
239
|
+
// 結果を上書きする」順序を防ぐ(#949・REQ-016-AC-13)。background 失敗時は appliedGen を進めないため、
|
|
240
|
+
// 後着の foreground SUCCESS は従来どおり適用される(#934 / AC-10 の失敗ケースを維持)。
|
|
241
|
+
// BranchTab は単一のグローバル資源(branch-status / sessions)を扱い ID 単位の識別がないため、
|
|
242
|
+
// appliedGen のリセットは不要(gen は単調増加のみ)
|
|
243
|
+
const fetchGenSeq = useRef(0);
|
|
244
|
+
const appliedGen = useRef(0);
|
|
245
|
+
const mountedRef = useRef(true);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
mountedRef.current = true;
|
|
249
|
+
return () => {
|
|
250
|
+
mountedRef.current = false;
|
|
251
|
+
};
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
// foreground 取得開始時は poll seq も進め、in-flight の background poll を無効化する。
|
|
255
|
+
// 併せて取得世代を採番する
|
|
256
|
+
const beginLoadRequest = useCallback(() => {
|
|
257
|
+
loadRequestSeq.current += 1;
|
|
258
|
+
pollRequestSeq.current += 1;
|
|
259
|
+
fetchGenSeq.current += 1;
|
|
260
|
+
return { requestId: loadRequestSeq.current, gen: fetchGenSeq.current };
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
// background poll は poll seq のみ進め、in-flight の foreground load は無効化しない
|
|
264
|
+
const beginPollRequest = useCallback(() => {
|
|
265
|
+
pollRequestSeq.current += 1;
|
|
266
|
+
fetchGenSeq.current += 1;
|
|
267
|
+
return { requestId: pollRequestSeq.current, gen: fetchGenSeq.current };
|
|
268
|
+
}, []);
|
|
204
269
|
|
|
205
270
|
const load = useCallback(() => {
|
|
271
|
+
const { requestId, gen } = beginLoadRequest();
|
|
206
272
|
dispatch({ type: "LOADING" });
|
|
207
273
|
Promise.all([fetchBranchStatusApi(), fetchSessionsApi()])
|
|
208
|
-
.then(([branchStatus, allSessions]) =>
|
|
209
|
-
|
|
210
|
-
|
|
274
|
+
.then(([branchStatus, allSessions]) => {
|
|
275
|
+
if (!mountedRef.current || requestId !== loadRequestSeq.current) return;
|
|
276
|
+
// より新しい取得が適用済みならデータは上書きしない(loading は poll の SUCCESS が解除済み)
|
|
277
|
+
if (gen <= appliedGen.current) return;
|
|
278
|
+
appliedGen.current = gen;
|
|
279
|
+
dispatch({ type: "SUCCESS", status: branchStatus, sessions: allSessions });
|
|
280
|
+
})
|
|
211
281
|
.catch((err: unknown) => {
|
|
212
|
-
if (!
|
|
213
|
-
|
|
214
|
-
}
|
|
282
|
+
if (!mountedRef.current || requestId !== loadRequestSeq.current) return;
|
|
283
|
+
if (isAbortError(err)) return;
|
|
284
|
+
dispatch({ type: "ERROR", message: getErrorMessage(err, "ブランチ情報の取得に失敗しました") });
|
|
215
285
|
});
|
|
216
|
-
}, []);
|
|
286
|
+
}, [beginLoadRequest]);
|
|
287
|
+
|
|
288
|
+
const poll = useCallback(() => {
|
|
289
|
+
const { requestId, gen } = beginPollRequest();
|
|
290
|
+
// Promise を返し、usePeriodicSync の in-flight ガード(#937)で並列 poll を防ぐ
|
|
291
|
+
return Promise.all([fetchBranchStatusApi(), fetchSessionsApi()])
|
|
292
|
+
.then(([branchStatus, allSessions]) => {
|
|
293
|
+
if (!mountedRef.current || requestId !== pollRequestSeq.current) return;
|
|
294
|
+
// より新しい取得が適用済みなら上書きしない(#949)
|
|
295
|
+
if (gen <= appliedGen.current) return;
|
|
296
|
+
appliedGen.current = gen;
|
|
297
|
+
dispatch({ type: "SUCCESS", status: branchStatus, sessions: allSessions });
|
|
298
|
+
})
|
|
299
|
+
.catch(() => {
|
|
300
|
+
// バックグラウンド同期の失敗は UI を乱さない(手動「再読み込み」でエラー表示)。
|
|
301
|
+
// loading は foreground load のみが管理するため、ここでは状態を更新しない
|
|
302
|
+
});
|
|
303
|
+
}, [beginPollRequest]);
|
|
217
304
|
|
|
218
305
|
useEffect(() => {
|
|
219
306
|
load();
|
|
220
307
|
}, [load]);
|
|
221
308
|
|
|
309
|
+
usePeriodicSync({
|
|
310
|
+
enabled: syncEnabled,
|
|
311
|
+
intervalMs: GITHUB_SYNC_POLL_INTERVAL_MS,
|
|
312
|
+
onSync: poll,
|
|
313
|
+
// マウント時の初回 load と即時 sync の二重 dispatch を回避(#929)
|
|
314
|
+
skipInitialSync: true,
|
|
315
|
+
});
|
|
316
|
+
|
|
222
317
|
const { status: branchStatus, sessions, loading, error } = state;
|
|
223
318
|
|
|
224
319
|
if (loading) {
|
|
@@ -239,7 +334,7 @@ function BranchTab() {
|
|
|
239
334
|
</div>
|
|
240
335
|
<button
|
|
241
336
|
type="button"
|
|
242
|
-
onClick={load}
|
|
337
|
+
onClick={() => load()}
|
|
243
338
|
className="mt-2 text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
|
244
339
|
>
|
|
245
340
|
<RefreshCw className="w-3 h-3" />
|
|
@@ -296,7 +391,7 @@ function BranchTab() {
|
|
|
296
391
|
<p className="text-xs font-medium text-gray-400">アクティブセッション</p>
|
|
297
392
|
<button
|
|
298
393
|
type="button"
|
|
299
|
-
onClick={load}
|
|
394
|
+
onClick={() => load()}
|
|
300
395
|
className="text-gray-500 hover:text-gray-300 transition-colors"
|
|
301
396
|
aria-label="再読み込み"
|
|
302
397
|
>
|
|
@@ -333,11 +428,31 @@ function BranchTab() {
|
|
|
333
428
|
);
|
|
334
429
|
}
|
|
335
430
|
|
|
336
|
-
export default function FileInfoPanel({
|
|
431
|
+
export default function FileInfoPanel({
|
|
432
|
+
fileContent,
|
|
433
|
+
collabUsers,
|
|
434
|
+
onClose,
|
|
435
|
+
panelResize,
|
|
436
|
+
syncEnabled = false,
|
|
437
|
+
}: Props) {
|
|
337
438
|
const [tab, setTab] = useState<Tab>("details");
|
|
338
439
|
|
|
339
440
|
return (
|
|
340
|
-
<aside
|
|
441
|
+
<aside
|
|
442
|
+
className={RESIZABLE_RIGHT_PANEL_ASIDE_CLASS}
|
|
443
|
+
style={panelResize != null ? { width: panelResize.width } : undefined}
|
|
444
|
+
>
|
|
445
|
+
{panelResize && panelResize.showResizeHandle !== false && (
|
|
446
|
+
<PanelResizeHandle
|
|
447
|
+
edge="inline-start"
|
|
448
|
+
onPointerDown={panelResize.onPointerDown}
|
|
449
|
+
width={panelResize.width}
|
|
450
|
+
minWidth={panelResize.minWidth}
|
|
451
|
+
maxWidth={panelResize.maxWidth}
|
|
452
|
+
onWidthChange={panelResize.onWidthChange}
|
|
453
|
+
ariaLabel="ファイル情報パネルの幅を変更"
|
|
454
|
+
/>
|
|
455
|
+
)}
|
|
341
456
|
{/* Header */}
|
|
342
457
|
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-700 shrink-0">
|
|
343
458
|
<h3 className="text-sm font-semibold text-gray-300">ファイル情報</h3>
|
|
@@ -375,7 +490,7 @@ export default function FileInfoPanel({ fileContent, collabUsers, onClose }: Pro
|
|
|
375
490
|
{tab === "history" && (
|
|
376
491
|
<HistoryTab filePath={fileContent.path} />
|
|
377
492
|
)}
|
|
378
|
-
{tab === "branch" && <BranchTab />}
|
|
493
|
+
{tab === "branch" && <BranchTab syncEnabled={syncEnabled} />}
|
|
379
494
|
</div>
|
|
380
495
|
</aside>
|
|
381
496
|
);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
vi.mock("@/lib/api", async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import("@/lib/api")>();
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
listIssueCommentsApi: vi.fn(),
|
|
9
|
+
createIssueCommentApi: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockShowToast = vi.hoisted(() => vi.fn());
|
|
14
|
+
vi.mock("@/contexts/ToastContext", () => ({
|
|
15
|
+
useToast: () => ({ showToast: mockShowToast }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/components/FileContentView", () => ({
|
|
19
|
+
default: ({ content }: { content: string }) => (
|
|
20
|
+
<div data-testid="comment-body">{content}</div>
|
|
21
|
+
),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { listIssueCommentsApi, createIssueCommentApi } from "@/lib/api";
|
|
25
|
+
import IssueCommentThread from "./IssueCommentThread";
|
|
26
|
+
|
|
27
|
+
const mockList = vi.mocked(listIssueCommentsApi);
|
|
28
|
+
const mockCreate = vi.mocked(createIssueCommentApi);
|
|
29
|
+
|
|
30
|
+
const COMMENT = {
|
|
31
|
+
id: 1001,
|
|
32
|
+
body: "Hello from GitHub",
|
|
33
|
+
author: "alice",
|
|
34
|
+
createdAt: "2026-05-19T10:00:00Z",
|
|
35
|
+
updatedAt: "2026-05-19T10:00:00Z",
|
|
36
|
+
url: "https://github.com/org/repo/issues/42#issuecomment-1001",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("IssueCommentThread", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
mockList.mockResolvedValue({ comments: [] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("コメント一覧を時系列で表示する", async () => {
|
|
46
|
+
mockList.mockResolvedValue({ comments: [COMMENT] });
|
|
47
|
+
render(<IssueCommentThread issueNumber={42} canEdit={true} />);
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(screen.getByText("alice")).toBeTruthy();
|
|
50
|
+
expect(screen.getByTestId("comment-body").textContent).toBe("Hello from GitHub");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("canEdit=true のとき投稿フォームを表示する", async () => {
|
|
55
|
+
render(<IssueCommentThread issueNumber={42} canEdit={true} />);
|
|
56
|
+
await waitFor(() => {
|
|
57
|
+
expect(screen.getByPlaceholderText("Markdown でコメントを入力…")).toBeTruthy();
|
|
58
|
+
expect(screen.getByText("コメント投稿")).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("canEdit=false のとき投稿フォームを非表示にする", async () => {
|
|
63
|
+
render(<IssueCommentThread issueNumber={42} canEdit={false} />);
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(screen.queryByPlaceholderText("Markdown でコメントを入力…")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("コメント投稿成功後に一覧へ追加する", async () => {
|
|
70
|
+
const newComment = { ...COMMENT, id: 1002, body: "Posted" };
|
|
71
|
+
mockCreate.mockResolvedValue(newComment);
|
|
72
|
+
render(<IssueCommentThread issueNumber={42} canEdit={true} />);
|
|
73
|
+
await waitFor(() => expect(screen.getByText("コメント投稿")).toBeTruthy());
|
|
74
|
+
|
|
75
|
+
fireEvent.change(screen.getByPlaceholderText("Markdown でコメントを入力…"), {
|
|
76
|
+
target: { value: "Posted" },
|
|
77
|
+
});
|
|
78
|
+
fireEvent.click(screen.getByText("コメント投稿"));
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(mockCreate).toHaveBeenCalledWith(42, "Posted");
|
|
82
|
+
expect(screen.getByText("Posted")).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("読み込み失敗時にエラーメッセージを表示する", async () => {
|
|
87
|
+
mockList.mockRejectedValue(new Error("GitHub API unavailable"));
|
|
88
|
+
render(<IssueCommentThread issueNumber={42} canEdit={true} />);
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByText("GitHub API unavailable")).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, lazy, Suspense } from "react";
|
|
2
|
+
import { MessageSquare, Send, ExternalLink } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
listIssueCommentsApi,
|
|
5
|
+
createIssueCommentApi,
|
|
6
|
+
getErrorMessage,
|
|
7
|
+
type IssueComment,
|
|
8
|
+
} from "@/lib/api";
|
|
9
|
+
import { useToast } from "@/contexts/ToastContext";
|
|
10
|
+
import { ContentSkeleton } from "@/components/Skeleton";
|
|
11
|
+
import { ISSUE_MOBILE_ACTION_BUTTON } from "@/lib/issue-mobile-layout";
|
|
12
|
+
|
|
13
|
+
const FileContentView = lazy(() => import("@/components/FileContentView"));
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
issueNumber: number;
|
|
17
|
+
canEdit: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function formatDate(iso: string): string {
|
|
21
|
+
const d = new Date(iso);
|
|
22
|
+
return d.toLocaleString("ja-JP", {
|
|
23
|
+
month: "2-digit",
|
|
24
|
+
day: "2-digit",
|
|
25
|
+
hour: "2-digit",
|
|
26
|
+
minute: "2-digit",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function IssueCommentThread({ issueNumber, canEdit }: Props) {
|
|
31
|
+
const { showToast } = useToast();
|
|
32
|
+
const [comments, setComments] = useState<IssueComment[]>([]);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
35
|
+
const [draft, setDraft] = useState("");
|
|
36
|
+
const [posting, setPosting] = useState(false);
|
|
37
|
+
|
|
38
|
+
const loadComments = useCallback(async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setLoadError(null);
|
|
41
|
+
try {
|
|
42
|
+
const data = await listIssueCommentsApi(issueNumber);
|
|
43
|
+
setComments(data.comments);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
setComments([]);
|
|
46
|
+
setLoadError(getErrorMessage(e, "コメントの読み込みに失敗しました"));
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
}, [issueNumber]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
void loadComments();
|
|
54
|
+
}, [loadComments]);
|
|
55
|
+
|
|
56
|
+
const handlePost = async () => {
|
|
57
|
+
const text = draft.trim();
|
|
58
|
+
if (!text) return;
|
|
59
|
+
setPosting(true);
|
|
60
|
+
try {
|
|
61
|
+
const created = await createIssueCommentApi(issueNumber, text);
|
|
62
|
+
setComments((prev) => [...prev, created]);
|
|
63
|
+
setDraft("");
|
|
64
|
+
showToast("コメントを投稿しました", "success");
|
|
65
|
+
} catch (e) {
|
|
66
|
+
showToast(`コメント投稿に失敗しました: ${getErrorMessage(e)}`, "error");
|
|
67
|
+
} finally {
|
|
68
|
+
setPosting(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="border-t border-gray-700 pt-4 mt-4">
|
|
74
|
+
<h2 className="text-sm font-semibold text-gray-300 flex items-center gap-2 mb-3">
|
|
75
|
+
<MessageSquare className="w-4 h-4" />
|
|
76
|
+
コメント
|
|
77
|
+
{!loading && (
|
|
78
|
+
<span className="text-gray-500 font-normal">({comments.length})</span>
|
|
79
|
+
)}
|
|
80
|
+
</h2>
|
|
81
|
+
|
|
82
|
+
{loading && <ContentSkeleton />}
|
|
83
|
+
|
|
84
|
+
{!loading && loadError && (
|
|
85
|
+
<p className="text-sm text-red-400">{loadError}</p>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{!loading && !loadError && comments.length === 0 && (
|
|
89
|
+
<p className="text-sm text-gray-500 italic">コメントはまだありません</p>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{!loading && !loadError && comments.length > 0 && (
|
|
93
|
+
<ul className="space-y-3 mb-4">
|
|
94
|
+
{comments.map((c) => (
|
|
95
|
+
<li
|
|
96
|
+
key={c.id}
|
|
97
|
+
className="rounded border border-gray-700 bg-gray-800/60 px-3 py-2 text-sm"
|
|
98
|
+
>
|
|
99
|
+
<div className="flex items-baseline justify-between gap-2 mb-1">
|
|
100
|
+
<span className="font-medium text-gray-200">{c.author}</span>
|
|
101
|
+
<div className="flex items-center gap-2 shrink-0 text-xs text-gray-500">
|
|
102
|
+
<span>{formatDate(c.createdAt)}</span>
|
|
103
|
+
{c.url && (
|
|
104
|
+
<a
|
|
105
|
+
href={c.url}
|
|
106
|
+
target="_blank"
|
|
107
|
+
rel="noopener noreferrer"
|
|
108
|
+
className="text-blue-400 hover:text-blue-300"
|
|
109
|
+
aria-label="GitHub で開く"
|
|
110
|
+
>
|
|
111
|
+
<ExternalLink className="w-3 h-3" />
|
|
112
|
+
</a>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{c.body ? (
|
|
117
|
+
<Suspense fallback={<ContentSkeleton />}>
|
|
118
|
+
<FileContentView
|
|
119
|
+
path={`issue-comment-${c.id}.md`}
|
|
120
|
+
content={c.body}
|
|
121
|
+
encoding="utf-8"
|
|
122
|
+
/>
|
|
123
|
+
</Suspense>
|
|
124
|
+
) : (
|
|
125
|
+
<p className="text-gray-500 italic text-xs">(空のコメント)</p>
|
|
126
|
+
)}
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{canEdit && (
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
<label className="block text-xs text-gray-400">コメントを追加</label>
|
|
135
|
+
<textarea
|
|
136
|
+
value={draft}
|
|
137
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
138
|
+
placeholder="Markdown でコメントを入力…"
|
|
139
|
+
rows={4}
|
|
140
|
+
disabled={posting}
|
|
141
|
+
className="w-full px-3 py-2 rounded bg-gray-900 border border-gray-600 text-gray-200 placeholder-gray-500 outline-none focus:border-blue-500 text-sm font-mono resize-y"
|
|
142
|
+
/>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={() => void handlePost()}
|
|
146
|
+
disabled={posting || !draft.trim()}
|
|
147
|
+
className={`${ISSUE_MOBILE_ACTION_BUTTON} bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center justify-center gap-1.5`}
|
|
148
|
+
>
|
|
149
|
+
<Send className="w-3.5 h-3.5" />
|
|
150
|
+
{posting ? "投稿中…" : "コメント投稿"}
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|