rpr-cli 0.1.26__tar.gz → 0.1.27__tar.gz
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.
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/PKG-INFO +1 -1
- rpr_cli-0.1.27/src/rpr/__init__.py +1 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/checks/instructions.py +1 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/add.py +156 -14
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/ui.py +10 -2
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/README.md +349 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/client/index.ts +1 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/client/types.ts +45 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/ChatComposer.spec.tsx +329 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/ChatComposer.tsx +358 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/__screenshots__/ChatComposer.spec.tsx/ChatComposer-renders-custom-action-buttons-beside-the-attachment-control-1.png +0 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/fileDrop.spec.ts +60 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/fileDrop.ts +128 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/index.ts +6 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatComposerMentionTokens.tsx +139 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatSuggestionsPopover.tsx +125 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/LocalFileSuggestionAction.tsx +100 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/index.ts +7 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.spec.ts +281 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.ts +1003 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.spec.ts +56 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.ts +58 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/types.ts +123 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/suggestions/useChatSuggestions.ts +339 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/types.ts +114 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/useChatComposer.ts +169 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/useChatStream.spec.tsx +104 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/composer/useChatStream.ts +212 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/index.ts +5 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/testing/createMockChatStreamClient.ts +91 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/testing/createMockSuggestionProvider.ts +99 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/testing/index.ts +2 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/ChatMessage.tsx +235 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.spec.tsx +163 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.tsx +670 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/ChatThread.spec.tsx +365 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/ChatThread.tsx +93 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/README.md +111 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/contentFormat.ts +30 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/index.ts +6 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/streaming.ts +174 -0
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/thread/types.ts +135 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component.spec.md → rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.spec.tsx +39 -60
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component.md → rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.tsx +12 -28
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component-list.spec.md → rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.spec.tsx +58 -85
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component-list.md → rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.tsx +32 -63
- rpr_cli-0.1.27/src/rpr/scaffolds/js_special_files/chat/virtual-list/index.ts +1 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/templates/registry.py +123 -51
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_add.py +201 -3
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_check.py +4 -0
- rpr_cli-0.1.26/src/rpr/__init__.py +0 -1
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-list-index.md +0 -9
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/.github/agents/story.agent.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/.github/workflows/publish-pypi.yml +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/.gitignore +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/CLAUDE.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/GEMINI.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/LICENSE +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/README.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/all.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/api.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/domain.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/frontend.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/rust-engine.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/setup-guide.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/tooling-setup.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/instructions/tooling.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/README.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/api/main.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/domain/base-repo.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/domain/base_entity.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/api.service.ts +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/useStateNavigation.ts +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponent.spec.tsx +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponent.tsx +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponentList.spec.tsx +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponentList.tsx +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/Sample-dot-github/scaffolds/frontend/virtual-list/index.ts +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/changelogs/mui_v9.txt +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/code-map.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/nx_uv_setup.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/patch_output.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/plan-languageAwareTemplateAdd.prompt.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/pyproject.toml +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/Cargo.lock +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/Cargo.toml +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/pyproject.toml +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/rpr_parser.pyi +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/config/container.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/config/mod.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/config/settings.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/graph.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/lib.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/models/dtos/mod.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/models/entities/mod.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/models/entities/users.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/models/mod.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/parse.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/repos/base.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/repos/mod.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/repos/users.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/resolve.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/src/walker.rs +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/rpr-parser/uv.lock +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/special_files.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/approval.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/bootstrap.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/client.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/control.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/mock.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/runtime.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/session.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/tools/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/tools/base.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/tools/mutating.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/tools/readonly.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/agent/tools/registry.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/catalog.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/chat_service.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/checks.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/cli_adapter.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/completer.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/conversation_service.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/prompt_service.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/selector.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/application/shell.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/checks/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/checks/base.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/checks/packages.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/checks/workspace.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/cli.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/chat.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/check.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/api.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/domain.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/engine.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/generate/storybook.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/init.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/map.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/refresh.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/settings.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/commands/sync.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/context.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/dependencies.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/dependency_manifest_data.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/base.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/claude.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/copilot.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/cursor.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/gemini.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/generators/github.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/manifest_refresh.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/architecture.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/chains.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/classifier.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/coverage.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/dependencies.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/extractor.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/graph.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/output.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/responsibility.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/topology.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/map/walker.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/domain/base_entity.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/domain/base_repo.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/domain/container.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/domain/settings.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/all.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/api.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/domain.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/frontend.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/rust-engine.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/setup-guide.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/tooling-setup.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/instructions/tooling.instructions.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/js_special_files/fetch.service.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/js_special_files/sticky-navigation.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/js_special_files/sticky-navigation.spec.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/js_special_files/vitest-browser-config.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/domain_container.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/domain_settings.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/dto_util.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/encrypted_column.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/mapper_util.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/scaffolds/special_files/partial_update.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/templates/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/console.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/markdown.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/prompt_session.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/renderers.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/ui/theme.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/src/rpr/workspace.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/README.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/00-bootstrap-cli.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/01-rpr-init.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/02-rpr-generate-domain.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/03-rpr-generate-api.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/04-rpr-generate-ui.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/05-rpr-generate-engine.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/06-rpr-generate-storybook.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/07-rpr-sync-rules.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/08-rpr-add-template.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/09-rpr-check.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/10-rpr-domain-config.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/18-rpr-terminal-ui-foundation.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/19-rpr-markdown-rendering.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/20-rpr-chat-repl.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/21-rpr-agent-tool-loop.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/22-rpr-chat-tui.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/23-rpr-chat-unified-command-shell.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/24-rpr-async-chat-runtime-and-services.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/25-rpr-prompt-service-and-conversation-lifecycle.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/26-rpr-shared-command-catalog.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/27-rpr-slash-command-selection-panel.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/28-rpr-chat-cancellable-and-multiline-input.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/chat/29-rpr-file-context-selection.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/11-rpr-map-command.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/12-rpr-map-file-walker.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/13-rpr-map-rust-engine.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/13a-rpr-map-gitignore.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/14-rpr-map-graph-builder.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/15-rpr-map-analysis-bridge.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/16-rpr-map-layer-classifier.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/17-rpr-map-output.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/17a-rpr-map-directory-topology.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/17b-rpr-map-architecture-assessment.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/17c-rpr-map-representative-chains.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/completed/map/17d-rpr-map-multi-resolution-output.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/map/30-rpr-map-directory-view-and-cycle-detection.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/map/31-rpr-map-coupling-hotspots-and-verdict-reasoning.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/stories/map/32-rpr-map-dependencies-responsibilities-and-test-coverage.md +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/__init__.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_architecture.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_architecture_unwrapped.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_catalog.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_chat.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_chat_service.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_completer.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_context.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_generate_api.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_generate_domain.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_generate_engine.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_generate_storybook.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_generate_ui.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_init.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_chains.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_classifier.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_coverage.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_dependencies.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_extractor.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_graph.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_output.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_responsibility.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_topology.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_map_walker.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_packaging.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_prompt_session.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_refresh.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_rpr_parser_stub.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_selector.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_settings.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_sync.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/tests/test_ui.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/update_output.py +0 -0
- {rpr_cli-0.1.26 → rpr_cli-0.1.27}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.27"
|
|
@@ -62,6 +62,13 @@ def template(
|
|
|
62
62
|
"--include-tests", help="Include unit-test companions when available."
|
|
63
63
|
),
|
|
64
64
|
] = False,
|
|
65
|
+
install: Annotated[
|
|
66
|
+
bool | None,
|
|
67
|
+
typer.Option(
|
|
68
|
+
"--install/--no-install",
|
|
69
|
+
help="Install npm dependencies after writing files (default depends on template).",
|
|
70
|
+
),
|
|
71
|
+
] = None,
|
|
65
72
|
output_dir: Annotated[
|
|
66
73
|
str | None,
|
|
67
74
|
typer.Option(
|
|
@@ -115,24 +122,43 @@ def template(
|
|
|
115
122
|
)
|
|
116
123
|
raise typer.Exit(1)
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
ordered_entries: list[tuple[TemplateEntry, bool]] = []
|
|
126
|
+
for required_id in entry.requires:
|
|
127
|
+
required_entry = TEMPLATES.get(required_id)
|
|
128
|
+
if required_entry is not None:
|
|
129
|
+
ordered_entries.append((required_entry, True))
|
|
130
|
+
ordered_entries.append((entry, False))
|
|
131
|
+
|
|
132
|
+
seen_targets: set[Path] = set()
|
|
133
|
+
rendered_files: list[tuple[TemplateFile, Path, str, bool]] = []
|
|
134
|
+
for current_entry, reuse_mode in ordered_entries:
|
|
135
|
+
files_for_entry = list(current_entry.files)
|
|
136
|
+
if include_tests:
|
|
137
|
+
files_for_entry.extend(current_entry.test_files)
|
|
138
|
+
files_for_entry.extend(current_entry.config_files)
|
|
139
|
+
for template_file in files_for_entry:
|
|
140
|
+
target_path = _resolve_target_path(
|
|
141
|
+
ctx, template_file, variables, output_dir
|
|
142
|
+
)
|
|
143
|
+
if target_path in seen_targets:
|
|
144
|
+
continue
|
|
145
|
+
seen_targets.add(target_path)
|
|
146
|
+
source_file = (
|
|
147
|
+
source_dir / template_file.source_subdir / template_file.source_filename
|
|
148
|
+
)
|
|
149
|
+
code = _render_template_file(source_file, template_file, variables)
|
|
150
|
+
rendered_files.append((template_file, target_path, code, reuse_mode))
|
|
129
151
|
|
|
130
152
|
skipped_config_paths: list[Path] = []
|
|
153
|
+
reused_paths: list[Path] = []
|
|
131
154
|
writable_files = []
|
|
132
|
-
for template_file, target_path, code in rendered_files:
|
|
155
|
+
for template_file, target_path, code, reuse_mode in rendered_files:
|
|
133
156
|
if dry_run or not target_path.exists() or force:
|
|
134
157
|
writable_files.append((template_file, target_path, code))
|
|
135
158
|
continue
|
|
159
|
+
if reuse_mode:
|
|
160
|
+
reused_paths.append(target_path)
|
|
161
|
+
continue
|
|
136
162
|
if template_file.role == "config":
|
|
137
163
|
console.print(
|
|
138
164
|
f"[bold yellow]Warning:[/] Config already exists, leaving it unchanged: {target_path}"
|
|
@@ -150,11 +176,24 @@ def template(
|
|
|
150
176
|
for _, target_path, code in writable_files:
|
|
151
177
|
ctx.write_file(target_path, code)
|
|
152
178
|
|
|
179
|
+
_update_root_index(
|
|
180
|
+
ctx,
|
|
181
|
+
[target_path for _, target_path, _ in writable_files],
|
|
182
|
+
variables,
|
|
183
|
+
output_dir,
|
|
184
|
+
)
|
|
185
|
+
|
|
153
186
|
_print_include_test_notes(entry, include_tests, skipped_config_paths)
|
|
154
187
|
|
|
188
|
+
_maybe_install_dependencies(ctx, ordered_entries, include_tests, install)
|
|
189
|
+
|
|
155
190
|
console.print("\n[bold green]Done![/] Template files written:")
|
|
156
191
|
for _, target_path, _ in writable_files:
|
|
157
192
|
console.print(f" {target_path}")
|
|
193
|
+
if reused_paths:
|
|
194
|
+
console.print("\n[bold]Reused existing files:[/]")
|
|
195
|
+
for target_path in reused_paths:
|
|
196
|
+
console.print(f" {target_path}")
|
|
158
197
|
if skipped_config_paths:
|
|
159
198
|
console.print("\n[bold yellow]Skipped existing config files:[/]")
|
|
160
199
|
for target_path in skipped_config_paths:
|
|
@@ -167,7 +206,9 @@ def _resolve_template_args(
|
|
|
167
206
|
if lang_or_template_id in KNOWN_TEMPLATE_LANGS:
|
|
168
207
|
return lang_or_template_id, template_id
|
|
169
208
|
if template_id is not None:
|
|
170
|
-
console.print(
|
|
209
|
+
console.print(
|
|
210
|
+
f"[bold red]Error:[/] Unknown template language: {lang_or_template_id}"
|
|
211
|
+
)
|
|
171
212
|
console.print("Supported languages: node, python")
|
|
172
213
|
raise typer.Exit(1)
|
|
173
214
|
return None, lang_or_template_id
|
|
@@ -233,6 +274,11 @@ def _render_template_file(
|
|
|
233
274
|
console.print(f"[bold red]Error:[/] Source file not found: {source_file}")
|
|
234
275
|
raise typer.Exit(1)
|
|
235
276
|
|
|
277
|
+
if template_file.raw:
|
|
278
|
+
code = source_file.read_text()
|
|
279
|
+
code = _interpolate_code(code, variables)
|
|
280
|
+
return _apply_raw_transforms(code, template_file, variables)
|
|
281
|
+
|
|
236
282
|
markdown = source_file.read_text()
|
|
237
283
|
code_blocks = _extract_code_blocks(markdown, template_file.language)
|
|
238
284
|
|
|
@@ -244,6 +290,32 @@ def _render_template_file(
|
|
|
244
290
|
return _interpolate_code(code, variables)
|
|
245
291
|
|
|
246
292
|
|
|
293
|
+
def _apply_raw_transforms(
|
|
294
|
+
code: str, template_file: TemplateFile, variables: dict[str, str]
|
|
295
|
+
) -> str:
|
|
296
|
+
"""Rewrite project-specific paths/imports in raw chat scaffold files."""
|
|
297
|
+
subdir = template_file.source_subdir
|
|
298
|
+
filename = template_file.source_filename
|
|
299
|
+
name_kebab = variables.get("name_kebab", "")
|
|
300
|
+
|
|
301
|
+
# The chat thread reuses the sibling standalone virtual-list component.
|
|
302
|
+
if subdir.endswith("chat/thread") and filename == "ChatThread.tsx":
|
|
303
|
+
code = code.replace("from '../virtual-list'", "from '../../virtual-list'")
|
|
304
|
+
|
|
305
|
+
# The chat barrel must not re-export the reused virtual-list package.
|
|
306
|
+
if subdir.endswith("js_special_files/chat") and filename == "index.ts":
|
|
307
|
+
code = code.replace("export * from './virtual-list';\n", "")
|
|
308
|
+
|
|
309
|
+
# Documentation references to the originating project should be templated.
|
|
310
|
+
if filename.endswith(".md"):
|
|
311
|
+
code = code.replace("packages/huddle-ui", f"packages/node/{name_kebab}-ui")
|
|
312
|
+
code = code.replace("packages/sb/", f"packages/node/{name_kebab}-sb/")
|
|
313
|
+
code = code.replace("@huddle/ui", f"@{name_kebab}/ui")
|
|
314
|
+
code = code.replace("huddle-ui", f"{name_kebab}-ui")
|
|
315
|
+
|
|
316
|
+
return code
|
|
317
|
+
|
|
318
|
+
|
|
247
319
|
def _interpolate_code(code: str, variables: dict[str, str]) -> str:
|
|
248
320
|
code = interpolate(code, variables)
|
|
249
321
|
name_val = variables.get("name", "")
|
|
@@ -255,6 +327,76 @@ def _interpolate_code(code: str, variables: dict[str, str]) -> str:
|
|
|
255
327
|
return code
|
|
256
328
|
|
|
257
329
|
|
|
330
|
+
def _update_root_index(
|
|
331
|
+
ctx: RunContext,
|
|
332
|
+
written_targets: list[Path],
|
|
333
|
+
variables: dict[str, str],
|
|
334
|
+
output_dir: str | None,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Add component barrel exports to the UI package root index when present."""
|
|
337
|
+
if output_dir:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
component_names: list[str] = []
|
|
341
|
+
for target in written_targets:
|
|
342
|
+
parts = target.parts
|
|
343
|
+
if "components" in parts:
|
|
344
|
+
index = parts.index("components")
|
|
345
|
+
if index + 1 < len(parts):
|
|
346
|
+
name = parts[index + 1]
|
|
347
|
+
if name not in component_names:
|
|
348
|
+
component_names.append(name)
|
|
349
|
+
if not component_names:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
index_rel = interpolate("packages/node/{name_kebab}-ui/src/index.ts", variables)
|
|
353
|
+
index_path = ctx.project_dir / index_rel
|
|
354
|
+
if not index_path.exists():
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
existing = index_path.read_text()
|
|
358
|
+
additions = [
|
|
359
|
+
f'export * from "./components/{name}";'
|
|
360
|
+
for name in sorted(component_names)
|
|
361
|
+
if f'"./components/{name}"' not in existing
|
|
362
|
+
]
|
|
363
|
+
if not additions:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
updated = existing.rstrip("\n") + "\n" + "\n".join(additions) + "\n"
|
|
367
|
+
ctx.write_file(index_path, updated)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _maybe_install_dependencies(
|
|
371
|
+
ctx: RunContext,
|
|
372
|
+
ordered_entries: list[tuple[TemplateEntry, bool]],
|
|
373
|
+
include_tests: bool,
|
|
374
|
+
install: bool | None,
|
|
375
|
+
) -> None:
|
|
376
|
+
install_by_default = any(entry.install_by_default for entry, _ in ordered_entries)
|
|
377
|
+
should_install = install_by_default if install is None else install
|
|
378
|
+
if not should_install:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
runtime: list[str] = []
|
|
382
|
+
dev: list[str] = []
|
|
383
|
+
for entry, _ in ordered_entries:
|
|
384
|
+
for dep in entry.dependencies:
|
|
385
|
+
if dep not in runtime:
|
|
386
|
+
runtime.append(dep)
|
|
387
|
+
if include_tests:
|
|
388
|
+
for dep in entry.dev_dependencies:
|
|
389
|
+
if dep not in dev:
|
|
390
|
+
dev.append(dep)
|
|
391
|
+
|
|
392
|
+
if runtime:
|
|
393
|
+
console.print("\n[bold]Installing runtime dependencies...[/]")
|
|
394
|
+
ctx.run_command(["npm", "i", *runtime], cwd=ctx.project_dir)
|
|
395
|
+
if dev:
|
|
396
|
+
console.print("\n[bold]Installing dev dependencies...[/]")
|
|
397
|
+
ctx.run_command(["npm", "i", "-D", *dev], cwd=ctx.project_dir)
|
|
398
|
+
|
|
399
|
+
|
|
258
400
|
def _resolve_target_path(
|
|
259
401
|
ctx: RunContext,
|
|
260
402
|
template_file: TemplateFile,
|
|
@@ -295,4 +437,4 @@ def _find_source_dir(config: SyncConfig) -> Path | None:
|
|
|
295
437
|
if common.exists():
|
|
296
438
|
return common
|
|
297
439
|
|
|
298
|
-
return None
|
|
440
|
+
return None
|
|
@@ -102,9 +102,17 @@ def ui(
|
|
|
102
102
|
if include_library:
|
|
103
103
|
console.print(" Optional templates: rpr add template node fetch_service")
|
|
104
104
|
console.print(" rpr add template node sticky_navigation")
|
|
105
|
-
console.print(
|
|
105
|
+
console.print(
|
|
106
|
+
" rpr add template node sticky_navigation --include-tests"
|
|
107
|
+
)
|
|
106
108
|
console.print(" rpr add template node virtual_list")
|
|
107
|
-
console.print(
|
|
109
|
+
console.print(
|
|
110
|
+
" rpr add template node virtual_list --include-tests"
|
|
111
|
+
)
|
|
112
|
+
console.print(" rpr add template node chat")
|
|
113
|
+
console.print(
|
|
114
|
+
" rpr add template node chat --include-tests"
|
|
115
|
+
)
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
def _scaffold_ui_library(ctx: RunContext, ui_pkg_name: str) -> None:
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Portable Chat UI
|
|
2
|
+
|
|
3
|
+
The `chat` directory is intended to be portable UI infrastructure. A consumer should be able to copy this directory into another React/MUI app, install the listed dependencies, inject an app-specific stream client, and get the same thread, composer, file attachment, and streaming behavior.
|
|
4
|
+
|
|
5
|
+
## What Is Included
|
|
6
|
+
|
|
7
|
+
- `thread` renders messages, content blocks, files, media, Markdown, HTML, math, code fences, avatars, and streaming chunks.
|
|
8
|
+
- `composer` renders the text/file input surface, native drag-and-drop handling, MUI file-icon attachments, generic trigger suggestions, send, and cancel controls.
|
|
9
|
+
- `client` defines the tiny injected streaming-client contract used by the composer hook.
|
|
10
|
+
- `testing` provides a deterministic mock streaming client for Storybook and unit tests.
|
|
11
|
+
- `virtual-list` keeps large message threads efficient.
|
|
12
|
+
|
|
13
|
+
The chat package should not import app-specific services, routes, auth state, stores, or generated API code directly.
|
|
14
|
+
|
|
15
|
+
## Dependencies
|
|
16
|
+
|
|
17
|
+
Install the UI/runtime dependencies used by this package:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @mui/material@latest @mui/icons-material@latest @emotion/react@latest @emotion/styled@latest
|
|
21
|
+
npm install dompurify react-markdown remark-gfm remark-math rehype-katex katex prism-react-renderer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The current project uses MUI 9. Keep MUI system styling in `sx` on MUI components because direct system props can leak to the DOM or fail generated declaration types in MUI 9.
|
|
25
|
+
|
|
26
|
+
## API Service Boundary
|
|
27
|
+
|
|
28
|
+
The portable chat package consumes a `ChatStreamClient`. It does not own `fetch`, auth tokens, backend URLs, or generated API-service code.
|
|
29
|
+
|
|
30
|
+
Apps can generate a fetch service outside the chat folder with:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
rpr add template node fetch_service --output-dir ./
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then wrap that generated service with the portable interface:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import type { ChatStreamClient } from '@huddle/ui';
|
|
40
|
+
|
|
41
|
+
export const chatStreamClient: ChatStreamClient = {
|
|
42
|
+
async send(request) {
|
|
43
|
+
// POST request.text, request.attachments, request.threadId, and metadata.
|
|
44
|
+
// The backend converts the request to SSE.
|
|
45
|
+
// The adapter parses SSE events and yields ChatMessageChunkDto objects.
|
|
46
|
+
return parseSseResponseAsChatChunks(response, request.signal);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use JSON for text-only requests when that fits the backend. Use `FormData` when files are present. The generated `api.service.ts` shown in this repo JSON-stringifies `post` bodies, so a real file upload adapter should add a multipart or raw `fetch` path instead of forcing `File` objects through JSON.
|
|
52
|
+
|
|
53
|
+
## Thread Ownership
|
|
54
|
+
|
|
55
|
+
`threadId` should normally be owned by the parent conversation controller, not by the composer. The composer hook can create and expose an active id when one is not provided, but selecting an old conversation, creating a new conversation, and managing multiple thread ids belongs above the portable chat package.
|
|
56
|
+
|
|
57
|
+
Recommended ownership:
|
|
58
|
+
|
|
59
|
+
- Parent app owns conversation selection and persistence.
|
|
60
|
+
- `useChatStream` accepts `threadId` and reports `activeThreadId`.
|
|
61
|
+
- The stream client sends the active id to the backend with each request.
|
|
62
|
+
|
|
63
|
+
## Composer Lifecycle
|
|
64
|
+
|
|
65
|
+
The composer supports text, file attachments, suggestions, send, and cancel.
|
|
66
|
+
|
|
67
|
+
1. User types a message or adds files.
|
|
68
|
+
2. User presses Enter or clicks send.
|
|
69
|
+
3. `useChatStream` appends an optimistic user message.
|
|
70
|
+
4. The send button becomes a stable-size progress ring with a stop icon inside.
|
|
71
|
+
5. The injected client sends the request with an `AbortSignal`.
|
|
72
|
+
6. SSE chunks are normalized to `ChatMessageChunkDto` and merged with `mergeChatMessageChunk`.
|
|
73
|
+
7. Clicking stop calls `cancel`, aborts the current request, and finalizes the visible partial response.
|
|
74
|
+
|
|
75
|
+
Use Shift+Enter for new lines. Enter sends when there is draft content.
|
|
76
|
+
|
|
77
|
+
The composer uses the full available width on mobile and a centered viewport width on larger screens: `width: { xs: '100%', sm: '80vw' }`, `maxWidth: '100%'`, `mx: 'auto'`, and `alignSelf: 'center'`. Override it at any time with `sx`:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<ChatComposer status={chat.status} onSend={chat.send} onCancel={chat.cancel} sx={{ width: { xs: '100%', sm: '72vw', lg: '900px' } }} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
File attachments in the composer use MUI file icons rather than pills. Current categories include PDF, image, video, audio, spreadsheet, code, text, document, and generic file.
|
|
84
|
+
|
|
85
|
+
## MUI Theming
|
|
86
|
+
|
|
87
|
+
Chat text is rendered through MUI primitives so app themes can control typography and color. Plain text uses `Typography`, Markdown text maps paragraphs/headings to `Typography`, Markdown links use MUI `Link`, and file labels use MUI text variants.
|
|
88
|
+
|
|
89
|
+
Use the MUI theme palette for semantic message states instead of hardcoded colors. Text content can opt into `tone: 'error'` or `tone: 'warning'`; message rows with `status: 'error'` render their text with `theme.palette.error.main`. System messages use `text.secondary`, and links use `primary.main`.
|
|
90
|
+
|
|
91
|
+
## Suggestion System
|
|
92
|
+
|
|
93
|
+
`ChatComposer` supports generic trigger suggestions through injected providers. The portable chat package does not assume suggestions are users; each provider can return people, files, repos, tickets, symbols, or any app-specific entity.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import type { ChatSuggestionProvider } from '@huddle/ui';
|
|
97
|
+
|
|
98
|
+
const peopleProvider: ChatSuggestionProvider = {
|
|
99
|
+
id: 'people',
|
|
100
|
+
label: 'People',
|
|
101
|
+
trigger: '@',
|
|
102
|
+
async getSuggestions({ query, signal }) {
|
|
103
|
+
const response = await fetch(`/api/people?q=${encodeURIComponent(query)}`, { signal });
|
|
104
|
+
return response.json();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
<ChatComposer status={chat.status} onSend={chat.send} onCancel={chat.cancel} suggestionsConfig={{ providers: [peopleProvider] }} />;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Multiple providers can share the same trigger. For example, `@` can search both people and files, and the popover groups results by provider label.
|
|
112
|
+
|
|
113
|
+
Suggestion items separate visible text from backend context:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
{
|
|
117
|
+
id: 'user-avery',
|
|
118
|
+
displayValue: 'Avery Lee',
|
|
119
|
+
insertValue: '@Avery Lee',
|
|
120
|
+
description: 'Product lead',
|
|
121
|
+
payload: { type: 'user', userId: 'user-avery' },
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The composer inserts people and other text-backed selections directly into the text field and stores a hidden mention DTO in `draft.mentions`. `useChatStream` forwards those values to the injected `ChatStreamClient` as `request.mentions`, so the backend can receive stable ids and payloads without parsing display text.
|
|
126
|
+
|
|
127
|
+
Supported controls:
|
|
128
|
+
|
|
129
|
+
- typing a trigger opens a loading popover next to the composer input
|
|
130
|
+
- further typing calls providers again with the new query
|
|
131
|
+
- Up and Down move the highlighted result
|
|
132
|
+
- Enter or Tab selects the highlighted result while the popover is open
|
|
133
|
+
- Escape closes the popover
|
|
134
|
+
- clicking a result selects it
|
|
135
|
+
- clicking outside closes it
|
|
136
|
+
- whitespace closes normal single-token mention queries
|
|
137
|
+
|
|
138
|
+
Useful configuration knobs include `debounceMs`, `maxSuggestions`, `closeOnEmptyResults`, `appendSpaceOnSelect`, `popupSx`, and `renderSuggestion`.
|
|
139
|
+
|
|
140
|
+
File suggestions can opt into the same attachment path as drag/drop by providing real browser file data and a resolver:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<ChatComposer
|
|
144
|
+
onSend={chat.send}
|
|
145
|
+
suggestionsConfig={{
|
|
146
|
+
providers: [filesProvider],
|
|
147
|
+
getAttachmentFromSuggestion: (item) => {
|
|
148
|
+
const payload = item.payload as { file?: File } | undefined;
|
|
149
|
+
return payload?.file;
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The resolver may return a `File`, `{ file }`, or `{ blob, name, mimeType, lastModified }`. When a suggestion resolves to a real file, the composer attaches the file and keeps the inserted text reference so the backend can see where the file was referenced in the message. Path-only suggestions, such as `{ path: 'docs/roadmap.md' }`, are metadata references; they are not equivalent to a dropped file unless the app resolves them to a real `File` or `Blob`. Selected suggestions still store mention metadata, so submitted user messages can render selected people inline and the backend can receive stable ids and ranges in `request.mentions`.
|
|
156
|
+
|
|
157
|
+
### Built-In Local File Suggestions
|
|
158
|
+
|
|
159
|
+
The portable chat package includes a frontend-only local file suggestion provider. It indexes a user-selected folder, ranks files for `@` suggestions, and attaches selected suggestions as real browser `File` objects.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { ChatComposer, LocalFileSuggestionAction, useLocalFileSuggestionProvider, type UseChatStreamResult } from '@huddle/ui';
|
|
163
|
+
|
|
164
|
+
function ChatInput({ chat }: { chat: UseChatStreamResult }) {
|
|
165
|
+
const fileSuggestions = useLocalFileSuggestionProvider({ maxIndexedFiles: 1500 });
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<ChatComposer
|
|
169
|
+
status={chat.status}
|
|
170
|
+
onSend={chat.send}
|
|
171
|
+
onCancel={chat.cancel}
|
|
172
|
+
actionButtons={<LocalFileSuggestionAction fileSuggestions={fileSuggestions} />}
|
|
173
|
+
suggestionsConfig={{
|
|
174
|
+
providers: [fileSuggestions.provider],
|
|
175
|
+
getAttachmentFromSuggestion: (item) => {
|
|
176
|
+
const payload = item.payload as { file?: File } | undefined;
|
|
177
|
+
return payload?.file;
|
|
178
|
+
},
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The browser cannot silently index the user's home directory on first load. The local file provider stores the last granted directory handle in IndexedDB and reuses it when the browser still has read permission. If permission needs a user gesture, `LocalFileSuggestionAction` prompts from the composer toolbar. Common generated and system entries such as `node_modules`, `.git`, `.DS_Store`, `Thumbs.db`, and `System Volume Information` are skipped by default, and per-entry read errors are skipped so one protected file does not fail the whole index.
|
|
186
|
+
|
|
187
|
+
### Composer Toolbar Actions
|
|
188
|
+
|
|
189
|
+
`ChatComposer` accepts `actionButtons?: React.ReactNode`. The prop name is plural because the node can be one icon button, an array, or a fragment containing multiple provider-specific actions.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<ChatComposer
|
|
193
|
+
onSend={chat.send}
|
|
194
|
+
actionButtons={
|
|
195
|
+
<>
|
|
196
|
+
<LocalFileSuggestionAction fileSuggestions={fileSuggestions} />
|
|
197
|
+
<RepoSuggestionAction provider={repoProvider} />
|
|
198
|
+
<TicketSuggestionAction provider={ticketProvider} />
|
|
199
|
+
</>
|
|
200
|
+
}
|
|
201
|
+
suggestionsConfig={{ providers: [fileSuggestions.provider, repoProvider, ticketProvider] }}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Each action component should own its own icon button, tooltip, disabled state, dialogs, hidden inputs, and provider-specific setup logic. `ChatComposer` only decides where toolbar actions render; it does not know what each suggestion provider needs to configure itself.
|
|
206
|
+
|
|
207
|
+
Custom providers follow the same pattern: expose a provider for `suggestionsConfig.providers`, then optionally expose a toolbar action when the provider needs user setup.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
const docsProvider: ChatSuggestionProvider = {
|
|
211
|
+
id: 'docs',
|
|
212
|
+
label: 'Docs',
|
|
213
|
+
trigger: '@',
|
|
214
|
+
async getSuggestions({ query, signal }) {
|
|
215
|
+
return searchDocs(query, { signal });
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function DocsSuggestionAction() {
|
|
220
|
+
return (
|
|
221
|
+
<Tooltip title="Configure docs search">
|
|
222
|
+
<IconButton size="small" aria-label="Configure docs search">
|
|
223
|
+
<ManageSearchIcon fontSize="small" />
|
|
224
|
+
</IconButton>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Latest Message Auto Scroll
|
|
231
|
+
|
|
232
|
+
`ChatThread` can scroll the latest message into view when messages change. This intentionally scrolls to the beginning of the newest message, which is better for assistant responses because the user lands at the top of the reply instead of the bottom of a growing stream.
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
<ChatThread messages={chat.messages} autoScrollToLatest />
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Defaults are `behavior: 'smooth'` and `block: 'start'`. Override them when needed:
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
<ChatThread messages={chat.messages} autoScrollToLatest autoScrollBehavior="auto" autoScrollBlock="nearest" />
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Native File Drag And Drop
|
|
245
|
+
|
|
246
|
+
The composer intentionally uses browser APIs directly instead of a drag/drop library.
|
|
247
|
+
|
|
248
|
+
The implementation uses:
|
|
249
|
+
|
|
250
|
+
- `dragenter`, `dragover`, `dragleave`, and `drop` for drop-zone state.
|
|
251
|
+
- `DataTransferItemList` for dropped items.
|
|
252
|
+
- `DataTransferItem.getAsFileSystemHandle()` when available for the modern File System Access API path.
|
|
253
|
+
- `DataTransferItem.getAsFile()` and `DataTransfer.files` as broad fallbacks.
|
|
254
|
+
- A hidden `<input type="file" multiple>` for keyboard access and browser fallback.
|
|
255
|
+
|
|
256
|
+
Attachment handling should dedupe files by `name`, `size`, and `lastModified`. Image object URLs must be revoked when removed, cleared, or unmounted. Directory drops are optional: support file handles when available, skip unsupported directories, and expose skipped items through a callback so the app can show a snackbar or inline note.
|
|
257
|
+
|
|
258
|
+
## Hook Usage
|
|
259
|
+
|
|
260
|
+
Prefer the object-returning API because it can grow without tuple churn:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
const chat = useChatStream({
|
|
264
|
+
client: chatStreamClient,
|
|
265
|
+
threadId,
|
|
266
|
+
userName: currentUser.name,
|
|
267
|
+
userAvatar: currentUser.avatar,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
<ChatThread messages={chat.messages} />
|
|
273
|
+
<ChatComposer status={chat.status} onSend={chat.send} onCancel={chat.cancel} />
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The earlier tuple idea, `const [send, cancel, threadId] = useChatStream(...)`, is intentionally not the canonical API. The feature needs status, messages, errors, active thread id, and setters, so the object API stays clearer.
|
|
279
|
+
|
|
280
|
+
## Mock Client
|
|
281
|
+
|
|
282
|
+
Use `createMockChatStreamClient` in Storybook and tests. It returns deterministic variable-length Markdown, code, math, and attachment-aware responses without a backend.
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const client = createMockChatStreamClient({ chunkDelayMs: 200, seed: 9 });
|
|
286
|
+
const chat = useChatStream({ client, createThreadId: () => 'storybook-thread' });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Use `createMockSuggestionProvider` to demo or test trigger suggestions without a search backend:
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
const providers = [createMockSuggestionProvider({ id: 'people', label: 'People' })];
|
|
293
|
+
|
|
294
|
+
<ChatComposer onSend={chat.send} suggestionsConfig={{ providers }} />;
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Testing Expectations
|
|
298
|
+
|
|
299
|
+
Required coverage for the full chat experience:
|
|
300
|
+
|
|
301
|
+
- composer text input and Enter-to-send behavior
|
|
302
|
+
- native dropped files through `DataTransferItemList`
|
|
303
|
+
- picker fallback through hidden file input
|
|
304
|
+
- unsupported directory-drop fallback behavior
|
|
305
|
+
- attachment dedupe and object URL cleanup
|
|
306
|
+
- optimistic user message append
|
|
307
|
+
- streamed assistant chunk merge and finalization
|
|
308
|
+
- cancel/abort behavior
|
|
309
|
+
- trigger suggestion loading, keyboard navigation, selection, outside-click close, and hidden mention payloads
|
|
310
|
+
- multiple thread id isolation
|
|
311
|
+
- error handling and retry affordances
|
|
312
|
+
- render performance boundaries
|
|
313
|
+
|
|
314
|
+
Performance tests should verify:
|
|
315
|
+
|
|
316
|
+
- typing in the composer does not rerender old thread rows
|
|
317
|
+
- drag hover state only rerenders the composer/drop zone
|
|
318
|
+
- attachment changes do not rerender old messages
|
|
319
|
+
- sending a new message appends only the new user row
|
|
320
|
+
- streaming chunks rerender only the active assistant row
|
|
321
|
+
|
|
322
|
+
## Storybook Expectations
|
|
323
|
+
|
|
324
|
+
Stories should include:
|
|
325
|
+
|
|
326
|
+
- text-only send
|
|
327
|
+
- native drag/drop attachments
|
|
328
|
+
- picker attachments
|
|
329
|
+
- slow cancellable stream
|
|
330
|
+
- `@` suggestions with one provider and multiple providers
|
|
331
|
+
- long Markdown/code/math responses
|
|
332
|
+
- error/retry state
|
|
333
|
+
- dark mode
|
|
334
|
+
- multiple thread ids
|
|
335
|
+
|
|
336
|
+
Current interactive stories live in `packages/sb/src/lib/huddle-ui/ChatComposer.stories.tsx`.
|
|
337
|
+
|
|
338
|
+
## Verification
|
|
339
|
+
|
|
340
|
+
Run these commands after changing the portable chat package:
|
|
341
|
+
|
|
342
|
+
```sh
|
|
343
|
+
npx nx test huddle-ui
|
|
344
|
+
npx nx build huddle-ui
|
|
345
|
+
npx nx build-storybook sb
|
|
346
|
+
npx nx storybook sb --port 6006
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
For portability, copy only `packages/huddle-ui/src/lib/chat` into a scratch React/MUI app and confirm no imports point back to the repo-specific `api.service.ts`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './types';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatMessageChunkDto, ChatMessageDto } from '../thread';
|
|
2
|
+
|
|
3
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
|
|
5
|
+
export type ChatStreamAttachmentInput = {
|
|
6
|
+
id: string;
|
|
7
|
+
file: File;
|
|
8
|
+
name: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
size: number;
|
|
11
|
+
lastModified: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ChatStreamMentionInput = {
|
|
15
|
+
id: string;
|
|
16
|
+
providerId: string;
|
|
17
|
+
providerLabel?: string;
|
|
18
|
+
trigger: string;
|
|
19
|
+
displayValue: string;
|
|
20
|
+
insertValue: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
avatarUrl?: string;
|
|
23
|
+
entityType?: string;
|
|
24
|
+
range?: {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
};
|
|
28
|
+
payload?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ChatStreamSendRequest = {
|
|
32
|
+
threadId: string;
|
|
33
|
+
message: ChatMessageDto;
|
|
34
|
+
text: string;
|
|
35
|
+
attachments: readonly ChatStreamAttachmentInput[];
|
|
36
|
+
mentions: readonly ChatStreamMentionInput[];
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ChatStreamClient = {
|
|
42
|
+
send: (
|
|
43
|
+
request: ChatStreamSendRequest
|
|
44
|
+
) => MaybePromise<AsyncIterable<ChatMessageChunkDto>>;
|
|
45
|
+
};
|