Conversation
Co-authored-by: JonnyTran <4750391+JonnyTran@users.noreply.github.com>
Co-authored-by: JonnyTran <4750391+JonnyTran@users.noreply.github.com>
- Update `BaseSimpleTable` adapter to pass the full column configuration (including editor params) instead of just name/type. - Update `RenderTable` logic to respect custom column settings (like `editor: "list"`) by prioritizing schema config over defaults. - Add watcher to `columnsConfig` in `RenderTable` to ensure dropdown options update reactively when new files are uploaded.
- Add missing `$emit('cell-edited')` in `RenderTable`'s tabulator handler.
- Ensures parent components are notified of changes to trigger state updates like the unmapped files counter.
…irectly injecting the token
Adds the CopilotConnect component to user settings, use-cases, and Pinia storage to handle the 3-step OAuth device flow.
There was a problem hiding this comment.
Pull request overview
Implements GitHub Copilot OAuth Device Flow connectivity and introduces supporting backend endpoints plus a new LiteLLM-based /chat API, alongside some packaging/runtime and import-flow updates.
Changes:
- Added backend GitHub device-flow auth endpoints and a new streaming chat endpoint using LiteLLM (with optional RAG retrieval).
- Added frontend Copilot “Connect” UI with repository/use-case/storage wiring in the Nuxt v1 architecture.
- Updated Python packaging constraints/dependencies and introduced PDF-only import metadata table state + tests (UI wiring appears incomplete).
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| extralit/pyproject.toml | Changes SDK Python requirement and dependency versions. |
| extralit-server/pyproject.toml | Bumps server Python requirement, adds LiteLLM + local extralit dependency. |
| extralit-server/src/extralit_server/utils/litellm_context.py | Adds per-user LiteLLM config isolation via XDG_CONFIG_HOME. |
| extralit-server/src/extralit_server/utils/auth_helpers.py | Implements GitHub device flow helper + token persistence. |
| extralit-server/src/extralit_server/jobs/preload.py | Enables OCR job preloading. |
| extralit-server/src/extralit_server/api/routes.py | Registers new /auth/github/* and /chat routers. |
| extralit-server/src/extralit_server/api/handlers/v1/github_auth.py | Adds GitHub auth API endpoints for device flow. |
| extralit-server/src/extralit_server/api/handlers/v1/chat.py | Adds streaming chat endpoint using LiteLLM + optional RAG. |
| extralit-frontend/v1/infrastructure/storage/GitHubAuthStorage.ts | Adds Pinia-backed reactive state for GitHub auth. |
| extralit-frontend/v1/infrastructure/repositories/index.ts | Exports the new GitHub auth repository. |
| extralit-frontend/v1/infrastructure/repositories/GitHubAuthRepository.ts | Adds API client for GitHub auth endpoints. |
| extralit-frontend/v1/domain/usecases/github-auth-use-case.ts | Adds polling orchestration and store updates for the device flow. |
| extralit-frontend/v1/di/di.ts | Registers new repository/use-case/storage in DI container. |
| extralit-frontend/components/features/user-settings/UserSettingsContent.vue | Adds GitHub Copilot section to user settings. |
| extralit-frontend/components/features/user-settings/useCopilotConnectViewModel.ts | View model for Copilot connect UI. |
| extralit-frontend/components/features/user-settings/CopilotConnect.vue | UI component for disconnected/pending/connected states. |
| extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts | Adds state/logic for a PDF-only editable metadata table (UI not wired in template). |
| extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.spec.ts | Adds unit tests for new import view-model logic. |
| extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue | Imports/registers BaseSimpleTable but doesn’t render it. |
| copilot_changes.txt | Contains a pasted patch/diff dump. |
| # requires-python = ">=3.9.2, <3.14" | ||
| requires-python = ">=3.11,<3.14" | ||
| readme = "README.md" | ||
| license = { text = "Apache 2.0" } |
There was a problem hiding this comment.
requires-python was raised to >=3.11, but repo docs indicate the extralit Python SDK supports Python 3.9.2+; this is a breaking packaging change for existing users/environments. Either keep the lower bound (or justify the bump) and ensure CI/docs/tooling are updated consistently.
| # requires-python = ">=3.9.2, <3.14" | |
| requires-python = ">=3.11,<3.14" | |
| readme = "README.md" | |
| license = { text = "Apache 2.0" } | |
| requires-python = ">=3.9.2, <3.14" | |
| readme = "README.md" | |
| license = { text = "Apache 2.0" } | |
| license = { text = "Apache 2.0" } |
| # requires-python = ">=3.11" | ||
| requires-python = ">=3.11,<3.14" | ||
| license = { text = "Apache-2.0" } |
There was a problem hiding this comment.
The server now requires Python >=3.11, but the Ruff config still targets py310 (and comments suggest older support). Please align the declared Python requirement, Ruff target-version, and any CI/runtime expectations to avoid incorrect linting/unsupported syntax usage.
| "pdf2image>=1.17.0", | ||
| "opencv-python-headless>=4.11.0.86", | ||
| # Local extralit package for OCR jobs | ||
| "extralit @ file:///${PROJECT_ROOT}/../extralit", |
There was a problem hiding this comment.
extralit @ file:///${PROJECT_ROOT}/../extralit is not a standard PEP 508 direct URL (env var interpolation is not generally supported by pip). This is likely to fail in non-dev installs/CI. Consider moving this to an optional/dev dependency, using a relative path supported by your package manager, or publishing/depending on a versioned extralit package instead.
| "extralit @ file:///${PROJECT_ROOT}/../extralit", | |
| "extralit @ ../extralit", |
| # Save original value | ||
| self.original_xdg_config_home = os.environ.get("XDG_CONFIG_HOME") | ||
|
|
||
| # Set to user-specific directory | ||
| os.environ["XDG_CONFIG_HOME"] = self.user_config_dir | ||
|
|
||
| # Ensure directory exists | ||
| Path(self.user_config_dir).mkdir(parents=True, exist_ok=True) | ||
|
|
There was a problem hiding this comment.
LiteLLMContext mutates the process-global os.environ["XDG_CONFIG_HOME"]. Under concurrent requests (multiple users), contexts can overlap and leak configuration across requests. Consider avoiding env mutation (preferred), or guard with a global async lock / per-request isolation mechanism that is concurrency-safe.
| def _get_config_dir(self) -> Path: | ||
| """Get the user-specific config directory for token storage.""" | ||
| config_path = Path(settings.home_path) / "data" / "users" / self.username / "config" | ||
| config_path.mkdir(parents=True, exist_ok=True) | ||
| return config_path |
There was a problem hiding this comment.
username is used directly as a path segment when building the config directory. Since username validation only enforces non-empty length, a crafted username containing path separators could escape the intended directory (path traversal). Please sanitize/normalize (e.g., restrict allowed chars) or use a safe identifier like user.id for filesystem paths.
| @@ -0,0 +1,201 @@ | |||
| import { ref } from "@nuxtjs/composition-api"; | |||
There was a problem hiding this comment.
Unused import: ref is imported but never referenced. This can fail lint/build in stricter configs and adds noise to the test file.
| import { ref } from "@nuxtjs/composition-api"; |
| diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue | ||
| index ff6874489..ab73e5afb 100644 | ||
| --- a/extralit-frontend/components/base/base-render-table/RenderTable.vue | ||
| +++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue | ||
| @@ -150,6 +150,7 @@ export default { | ||
| // } | ||
| // }, | ||
| // }, | ||
| + | ||
| validation: { | ||
| handler(newValidation, oldValidation) { | ||
| if (this.isLoaded) { | ||
| @@ -196,7 +197,7 @@ export default { | ||
| var configs = this.tableJSON.schema.fields.map((column: DataFrameField) => { | ||
| const commonConfig = this.generateColumnConfig(column.name); | ||
| const editableConfig = this.generateColumnEditableConfig(column.name); | ||
| - return { ...commonConfig, ...editableConfig }; | ||
| + return { ...commonConfig, ...editableConfig, ...column }; | ||
| }); | ||
|
|
||
| if (!this.editable) { | ||
| @@ -913,6 +914,7 @@ export default { | ||
|
|
||
| this.tabulator.on("cellEdited", (cell: CellComponent) => { | ||
| this.updateTableJsonData(); | ||
| + this.$emit("cell-edited", cell); | ||
| // const rowPos: number | boolean = cell.getRow().getPosition(); | ||
| // if (typeof rowPos != 'number' || rowPos < 0 || rowPos > this.tableJSON.data.length) return; | ||
| // this.$set(this.tableJSON.data[rowPos-1], cell.getColumn().getField(), cell.getValue()); | ||
| diff --git a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | ||
| index 15a62bbc4..3a7e5435a 100644 | ||
| --- a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | ||
| +++ b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | ||
| @@ -6,6 +6,7 @@ | ||
| :editable="editable" | ||
| :hasValidValues="hasValidValues" | ||
| :questions="questions" | ||
| + :validation="validation || validators" | ||
| @table-built="$emit('table-built')" | ||
| @row-click="(e, row) => $emit('row-click', e, row)" | ||
| @cell-edited="(cell) => $emit('cell-edited', cell)" | ||
| @@ -71,9 +72,11 @@ export default { | ||
| computed: { | ||
| // Convert simple data/columns to TableData format for RenderTable | ||
| computedTableJSON(): TableData { | ||
| + // FIX 1: Use "...col" to preserve editor config (dropdowns), validators, and freezing | ||
| const fields = this.columns.map((col: any) => ({ | ||
| name: col.field, | ||
| type: col.type || "string", | ||
| + ...col // <--- THIS IS THE MAGIC FIX | ||
| })); | ||
|
|
||
| const schema = new DataFrameSchema( | ||
| @@ -89,7 +92,14 @@ export default { | ||
| null | ||
| ); | ||
|
|
||
| - if (this.validation) { | ||
| + // FIX 2: Handle both 'validation' and 'validators' props | ||
| + // ImportFileUpload passes :validators, so we must prioritize that. |
There was a problem hiding this comment.
This file appears to be a pasted diff/patch dump (diff --git ...) rather than application code or documentation used by the project. If it’s only for reference, it shouldn’t be committed to the repo; please remove it or convert it into proper applied changes / documentation.
| diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue | |
| index ff6874489..ab73e5afb 100644 | |
| --- a/extralit-frontend/components/base/base-render-table/RenderTable.vue | |
| +++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue | |
| @@ -150,6 +150,7 @@ export default { | |
| // } | |
| // }, | |
| // }, | |
| + | |
| validation: { | |
| handler(newValidation, oldValidation) { | |
| if (this.isLoaded) { | |
| @@ -196,7 +197,7 @@ export default { | |
| var configs = this.tableJSON.schema.fields.map((column: DataFrameField) => { | |
| const commonConfig = this.generateColumnConfig(column.name); | |
| const editableConfig = this.generateColumnEditableConfig(column.name); | |
| - return { ...commonConfig, ...editableConfig }; | |
| + return { ...commonConfig, ...editableConfig, ...column }; | |
| }); | |
| if (!this.editable) { | |
| @@ -913,6 +914,7 @@ export default { | |
| this.tabulator.on("cellEdited", (cell: CellComponent) => { | |
| this.updateTableJsonData(); | |
| + this.$emit("cell-edited", cell); | |
| // const rowPos: number | boolean = cell.getRow().getPosition(); | |
| // if (typeof rowPos != 'number' || rowPos < 0 || rowPos > this.tableJSON.data.length) return; | |
| // this.$set(this.tableJSON.data[rowPos-1], cell.getColumn().getField(), cell.getValue()); | |
| diff --git a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | |
| index 15a62bbc4..3a7e5435a 100644 | |
| --- a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | |
| +++ b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue | |
| @@ -6,6 +6,7 @@ | |
| :editable="editable" | |
| :hasValidValues="hasValidValues" | |
| :questions="questions" | |
| + :validation="validation || validators" | |
| @table-built="$emit('table-built')" | |
| @row-click="(e, row) => $emit('row-click', e, row)" | |
| @cell-edited="(cell) => $emit('cell-edited', cell)" | |
| @@ -71,9 +72,11 @@ export default { | |
| computed: { | |
| // Convert simple data/columns to TableData format for RenderTable | |
| computedTableJSON(): TableData { | |
| + // FIX 1: Use "...col" to preserve editor config (dropdowns), validators, and freezing | |
| const fields = this.columns.map((col: any) => ({ | |
| name: col.field, | |
| type: col.type || "string", | |
| + ...col // <--- THIS IS THE MAGIC FIX | |
| })); | |
| const schema = new DataFrameSchema( | |
| @@ -89,7 +92,14 @@ export default { | |
| null | |
| ); | |
| - if (this.validation) { | |
| + // FIX 2: Handle both 'validation' and 'validators' props | |
| + // ImportFileUpload passes :validators, so we must prioritize that. | |
| # This file previously contained a temporary git diff/patch dump. | |
| # The changes have been or should be applied to the actual source files, | |
| # so the diff content has been removed to avoid keeping raw patches in the repo. |
| from extralit_server.api.handlers.v1 import authentication as authentication_v1 | ||
| from extralit_server.api.handlers.v1 import ( | ||
| chat as chat_v1, | ||
| ) |
There was a problem hiding this comment.
PR description frames this as a frontend-only OAuth Device Flow UI/data-layer change, but this diff also wires new backend routers (/auth/github/*, /chat) into the server API. Please either update the PR description/scope (and any linked tickets) to reflect backend + dependency/runtime changes, or split backend changes into a separate PR for easier review/release management.
| <UserTokenComponent :userToken="user.apiKey" /> | ||
| </div> | ||
| <div class="form-group"> | ||
| <h2 class="--heading5 --medium description__title">GitHub Copilot</h2> |
There was a problem hiding this comment.
This new section title is hardcoded (GitHub Copilot) while the rest of the settings page uses $t(...) for i18n. Please add the corresponding translation key and use $t here for consistency with the existing localization approach.
| <h2 class="--heading5 --medium description__title">GitHub Copilot</h2> | |
| <h2 class="--heading5 --medium description__title" v-text="$t('userSettings.copilot')" /> |
| github_auth = GitHubDeviceFlowAuth(username=current_user.username) | ||
|
|
||
| try: | ||
| # Poll with a short timeout (single attempt) | ||
| token_data = await github_auth.poll_for_token( | ||
| device_code=request.device_code, | ||
| interval=5, | ||
| timeout=10, # Just one quick check | ||
| ) | ||
|
|
||
| # Save the token | ||
| github_auth.save_token(token_data) | ||
|
|
||
| _LOGGER.info(f"Successfully authorized and saved token for {current_user.username}") | ||
|
|
||
| return PollTokenResponse( | ||
| status="authorized", | ||
| message="Successfully authenticated with GitHub", | ||
| ) | ||
| except TimeoutError: | ||
| # User hasn't authorized yet | ||
| return PollTokenResponse( | ||
| status="pending", | ||
| message="Authorization pending", | ||
| ) | ||
| except ValueError as e: | ||
| _LOGGER.error(f"Token polling error for {current_user.username}: {e}") | ||
| return PollTokenResponse( | ||
| status="error", | ||
| message=str(e), | ||
| ) |
There was a problem hiding this comment.
poll_github_token only catches TimeoutError and ValueError, but poll_for_token() can raise httpx.HTTPError (network/API failures) or JSON parsing exceptions. Those will currently surface as 500s. Consider catching httpx.HTTPError and returning an appropriate HTTPException/structured status="error" response.
[Feature] Implement GitHub Copilot OAuth Device Flow UI
Description
This PR adds the frontend UI and data layer to handle the 3-step GitHub Copilot OAuth Device Flow. This connects the client to the
/api/v1/auth/githubendpoints.Key Additions:
CopilotConnect.vuecomponent and added it to theUserSettingsContent.vuepage to handle the Disconnected, Pending (Polling), and Connected states.GitHubAuthRepositoryusing the standardNuxtAxiosInstanceto ensure Extralit JWTs are correctly passed for user isolation.GitHubAuthUseCaseto orchestrate the login initiation, status checking, and the background polling loop (with properonBeforeUnmountcleanup to prevent memory leaks).GitHubAuthStorageusing the existing PiniauseStoreForfactory pattern to handle the reactive auth state.Related Tickets & Documents
Depends on: #200
Steps to QA
extralit-frontendandextralit-serverdev servers are running.Added/updated tests?
Added/updated documentations?
Checklist