Skip to content

feat(workspace): document portability and private reuse - #63

Closed
Programcode wants to merge 18 commits into
mainfrom
feat/workspace-document-portability
Closed

Programcode wants to merge 18 commits into
mainfrom
feat/workspace-document-portability

Conversation

@Programcode

@Programcode Programcode commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Outcome

  • adds private snippets and templates with independent-copy insertion
  • imports Markdown, DOCX, and all Google Docs tabs with provenance
  • exports individual DOCX/PDF files and captured workspace packets
  • adds multiple Google account connections with drive.file, encrypted refresh tokens, PKCE, state, and bounded creation claims
  • adds a bounded private Tailscale trial lifecycle without exposing local admin services

Working journey

  1. Save selected writing as a private snippet or the document as a template.
  2. Open another workspace and insert an independent copy.
  3. Import Markdown or DOCX, review the preview, and replace the document.
  4. Export the document or a captured multi-document packet as DOCX/PDF.
  5. Connect a Google account when approved credentials are available.

Verification

  • exact branch head: d52598cc0964d25dd3329df4db43f441c99a0a0b
  • exact-head quality, database, and browser CI passed
  • exact-head substantive review found no Critical or Important issue
  • private trial: https://codex-remote.tailab0ab3.ts.net:18443 (Tailnet only, 12-hour lifecycle)
  • private HTTPS trial passed desktop portability, two-user collaboration, evidence/practice reload, and mobile revoke journeys
  • private route returned HTTPS 200, WebSocket 101, sensitive path 404, and rejected unexpected hosts

Environment limits

  • live Google OAuth is blocked because no approved local OAuth client, callback, encryption key, app origin, or Picker key is configured
  • no staging or production Supabase migration/function deployment was performed
  • CodeRabbit Free may provide summary-only review; a substantive exact-head reviewer is requested separately

This is the final workspace PR and must remain unmerged until Adam explicitly approves the merge.

Summary by CodeRabbit

  • New Features

    • Save selected text as private snippets or full documents as private templates.
    • Manage private writing items from the Library, including editing and deletion.
    • Insert saved content into workspace documents independently.
    • Import Markdown and DOCX files with previews and conversion warnings.
    • Export documents as DOCX or PDF, including practice-feedback packets.
    • Connect Google Drive to import and export Google Docs.
    • Preserve formatting, links, lists, tables, and provenance during conversions.
  • Localization

    • Added workspace portability translations in English, Spanish, and Hungarian.

RetriggerConfidence Score: 2/5

The PR is not yet safe to merge because imports can leave false provenance, Google exports can be blocked after successful creation, and cancelling Picker can permanently hang the portability panel.

Fix All in CodexFindings

  1. P1 Import Can Lose Content ▶
  2. P1 Export Popup Gets Blocked ▶
  3. P1 Picker Cancellation Hangs Panel ▶
  4. P2 Failed Delete Reports Success ▶
  5. P2 Timeout Leaves Conversion Running ▶
  6. P2 Mixed Lists Lose Structure ▶
  7. P2 Trial Boundary Is Undocumented ▶
Fix with agent prompt
### Issue 1
src/components/features/workspace/WorkspacePortabilityPanel.tsx:138-139
When the editor is offline or still waiting for an acknowledgement, this saves the import provenance before the replacement can be applied. If the user switches documents while the replacement is queued, `WorkspacePage` clears it, leaving durable provenance for content that was never imported. The requested replacement is lost and exported packets can report misleading provenance.

### Issue 2
src/components/features/workspace/WorkspacePortabilityPanel.tsx:197-199
The new tab is opened only after the Edge Function and Google API work completes. By then, the original click's user activation has expired, so popup-blocking browsers can suppress the tab even though the Google document was successfully created. Because the panel does not show a fallback link, the user cannot open the resulting document from this flow.

### Issue 3
src/lib/workspace/google-drive.ts:86-91
When the user closes or cancels Google Picker, the callback receives a non-`PICKED` action and returns without resolving or rejecting this Promise. The portability panel therefore never reaches its `finally` block, keeps `busy` set to true, and leaves its controls disabled until the page is reloaded.

### Issue 4
supabase/functions/google-connections/index.ts:147-148
The database delete result is ignored, so a transient database failure still returns `disconnected: true`. The client then removes the account from its current view even though the connection and encrypted token row remain, causing the account to reappear after a later refetch.

### Issue 5
src/lib/workspace/document-conversion.ts:511-514
This race rejects the visible import after five seconds but does not cancel Mammoth's decompression or conversion work. A costly user-selected DOCX can therefore continue consuming browser CPU and memory after the timeout, and repeated attempts can leave multiple conversions running at once.

### Issue 6
src/lib/workspace/document-conversion.ts:197-214
The parser chooses the list type from the first marker but then continues consuming both numbered and bullet markers. Importing adjacent mixed lists such as `1. First` followed by `- Separate` therefore turns the bullet into another numbered item instead of preserving two distinct lists.

### Issue 7
.superset/workspace-trial.sh:8-11
This change introduces a Tailscale-served origin, three dedicated ports, and a new Nginx runtime boundary. The repository directive requires the owning focused documentation to be updated whenever commands, ports, workflows, or runtime boundaries change. That required documentation must be added before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds private writing reuse, document import/export and packet generation, multi-account Google Drive integration, database support, and a private Tailscale trial runtime.

  • Adds private snippets and templates with independent-copy insertion and Library management.
  • Adds Markdown, DOCX, Google Docs, DOCX/PDF, and packet conversion flows.
  • Adds encrypted Google OAuth connections, refresh and creation claims, RLS policies, and Edge Functions.
  • Adds trial proxy/runtime configuration and expanded database, Edge Function, conversion, and browser coverage.
  • The review found blocking lifecycle issues in import queuing, Google export navigation, and Picker cancellation, plus several lower-impact reliability and documentation concerns.
Diagram
sequenceDiagram
  actor User
  participant UI as Workspace portability UI
  participant Editor as Collaborative editor
  participant DB as Supabase
  participant Edge as Google Edge Functions
  participant Google as Google APIs

  User->>UI: Save snippet/template
  UI->>DB: Store private writing item

  User->>UI: Confirm imported document
  UI->>DB: Save provenance
  UI->>Editor: Queue independent replacement
  Editor-->>DB: Persist collaborative update

  User->>UI: Connect Google account
  UI->>Edge: Start OAuth with PKCE/state
  Edge->>Google: Exchange authorization code
  Edge->>DB: Store encrypted connection tokens

  User->>UI: Import or export Google Doc
  UI->>Edge: Invoke document operation
  Edge->>DB: Claim refresh/creation operation
  Edge->>Google: Read or create document
  Edge-->>UI: Imported tabs or exported URL

  User->>UI: Export workspace packet
  UI->>DB: Capture member-authorized manifest
  UI->>UI: Generate DOCX or PDF
Loading

Reviews (1) · Last reviewed commit: "fix(workspace): route Deno boundary test..."

Copilot AI lite review requested due to automatic review settings September 22, 2026 20:23
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Sep 22, 2026 8:42pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9a5bd495-52c2-4a29-8cb4-ca14734a591c

📥 Commits

Reviewing files that changed from the base of the PR and between 7f58ee6 and d52598c.

📒 Files selected for processing (5)
  • .superset/workspace-trial.sh
  • supabase/functions/_shared/google-connections.test.ts
  • supabase/functions/_shared/google-cors.test.ts
  • supabase/functions/workspace-google-docs/index.ts
  • vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • supabase/functions/_shared/google-cors.test.ts
  • supabase/functions/_shared/google-connections.test.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Adds workspace portability for private writing, document conversion, packet export, and Google Drive. Adds persistence, OAuth-backed Edge Functions, UI workflows, localization, tests, and a local trial deployment harness.

Changes

Workspace portability and Google Drive

Layer / File(s) Summary
Persistence and portability clients
supabase/migrations/..., src/lib/workspace/portability-client.ts, supabase/tests/workspace_portability_test.sql
Adds private writing and provenance storage, export manifests, Google operation claims, access controls, validated client helpers, and database tests.
Document conversion and packet export
src/lib/workspace/document-conversion.ts, src/test/workspace/document-conversion.test.ts
Adds ordered-list, link, import-preview, warning, validation-limit, PDF, DOCX, and workspace packet conversion support.
Google connection and document services
supabase/functions/_shared/*, supabase/functions/google-connections/*, supabase/functions/workspace-google-docs/*, supabase/config.toml, supabase/README.md, .env.example, package.json
Adds encrypted OAuth credentials, token refresh coordination, Google Docs import, idempotent export, CORS coverage, function configuration, and environment documentation.
Workspace portability UI
src/components/features/workspace/*, src/lib/workspace/google-drive.ts, src/pages/WorkspacePage.tsx, src/pages/LibraryPage.tsx, src/locales/*
Adds private snippets and templates, document import and export, packet export, Google Drive controls, editor insertion callbacks, library management, and translations.

Workspace trial deployment and test harness

Layer / File(s) Summary
Trial runtime and proxy
.superset/workspace-trial.sh, .superset/workspace-trial-nginx.conf
Adds local service startup, Nginx routing, readiness checks, Tailscale forwarding, process tracking, locking, and cleanup.
Browser and end-to-end test support
e2e/workspace-collaboration.spec.ts, playwright.workspace.config.ts, scripts/workspace-test-env.mjs, vitest.config.ts
Adds reusable collaboration startup, browser host resolver rules, configurable fixture origins, and portability end-to-end coverage.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WorkspacePage
  participant WorkspacePortabilityPanel
  participant Supabase
  participant GoogleDrive
  User->>WorkspacePortabilityPanel: select import, export, or Google Drive action
  WorkspacePortabilityPanel->>Supabase: load or save portability data
  Supabase-->>WorkspacePortabilityPanel: validated items, manifest, or operation status
  WorkspacePortabilityPanel->>WorkspacePage: request content insertion or replacement
  WorkspacePage->>User: update workspace document
  WorkspacePortabilityPanel->>GoogleDrive: import or export document when selected
  GoogleDrive-->>WorkspacePortabilityPanel: document metadata or URL
Loading

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment on lines +138 to +139
await saveWorkspaceDocumentProvenance({ documentId, provenance: preview.provenance, sourceKind: preview.sourceKind, workspaceId });
onInsertContent(preview.content, "replace");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Import can lose content

When the editor is offline or still waiting for an acknowledgement, this saves the import provenance before the replacement can be applied. If the user switches documents while the replacement is queued, WorkspacePage clears it, leaving durable provenance for content that was never imported. The requested replacement is lost and exported packets can report misleading provenance.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/features/workspace/WorkspacePortabilityPanel.tsx
Line: 138-139

Comment:
**Import can lose content**

When the editor is offline or still waiting for an acknowledgement, this saves the import provenance before the replacement can be applied. If the user switches documents while the replacement is queued, `WorkspacePage` clears it, leaving durable provenance for content that was never imported. The requested replacement is lost and exported packets can report misleading provenance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines +197 to +199
const exported = await exportGoogleDocument({ connectionId: selectedConnectionId, markdown: toWorkspaceMarkdown(content), requestId: googleExportRequestId, title: documentTitle });
setGoogleExportRequestId(crypto.randomUUID());
window.open(exported.url, "_blank", "noopener,noreferrer");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Export popup gets blocked

The new tab is opened only after the Edge Function and Google API work completes. By then, the original click's user activation has expired, so popup-blocking browsers can suppress the tab even though the Google document was successfully created. Because the panel does not show a fallback link, the user cannot open the resulting document from this flow.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/features/workspace/WorkspacePortabilityPanel.tsx
Line: 197-199

Comment:
**Export popup gets blocked**

The new tab is opened only after the Edge Function and Google API work completes. By then, the original click's user activation has expired, so popup-blocking browsers can suppress the tab even though the Google document was successfully created. Because the panel does not show a fallback link, the user cannot open the resulting document from this flow.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines +86 to +91
.setCallback((data) => {
if (data[picker.Response.ACTION] !== picker.Action.PICKED) return;
const selected = (data[picker.Response.DOCUMENTS] as Array<Record<string, unknown>> | undefined)?.[0];
if (!selected?.id) reject(new Error("Google Picker returned no document"));
else resolve({ id: String(selected.id), name: String(selected.name ?? "Google document") });
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Picker cancellation hangs panel

When the user closes or cancels Google Picker, the callback receives a non-PICKED action and returns without resolving or rejecting this Promise. The portability panel therefore never reaches its finally block, keeps busy set to true, and leaves its controls disabled until the page is reloaded.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/workspace/google-drive.ts
Line: 86-91

Comment:
**Picker cancellation hangs panel**

When the user closes or cancels Google Picker, the callback receives a non-`PICKED` action and returns without resolving or rejecting this Promise. The portability panel therefore never reaches its `finally` block, keeps `busy` set to true, and leaves its controls disabled until the page is reloaded.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines +147 to +148
await auth.adminClient.from("google_connections").delete().eq("id", connectionId).eq("user_id", auth.user.id);
return json({ disconnected: true }, 200, headers);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed delete reports success

The database delete result is ignored, so a transient database failure still returns disconnected: true. The client then removes the account from its current view even though the connection and encrypted token row remain, causing the account to reappear after a later refetch.

Prompt To Fix With AI
This is a comment left during a code review.
Path: supabase/functions/google-connections/index.ts
Line: 147-148

Comment:
**Failed delete reports success**

The database delete result is ignored, so a transient database failure still returns `disconnected: true`. The client then removes the account from its current view even though the connection and encrypted token row remain, causing the account to reappear after a later refetch.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines +511 to +514
const converted = await Promise.race([
convertWorkspaceDocx(bytes),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("DOCX parsing timed out.")), 5_000)),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timeout leaves conversion running

This race rejects the visible import after five seconds but does not cancel Mammoth's decompression or conversion work. A costly user-selected DOCX can therefore continue consuming browser CPU and memory after the timeout, and repeated attempts can leave multiple conversions running at once.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/workspace/document-conversion.ts
Line: 511-514

Comment:
**Timeout leaves conversion running**

This race rejects the visible import after five seconds but does not cancel Mammoth's decompression or conversion work. A costly user-selected DOCX can therefore continue consuming browser CPU and memory after the timeout, and repeated attempts can leave multiple conversions running at once.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines 197 to 214
@@ -202,11 +208,14 @@ function parseList(lines: string[], start: number, indent: number) {
}
items.push({
type: "listItem",
content: [{ type: "paragraph", content: parseInline(match[2]) }],
content: [{ type: "paragraph", content: parseInline(match[3]) }],
});
index += 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mixed lists lose structure

The parser chooses the list type from the first marker but then continues consuming both numbered and bullet markers. Importing adjacent mixed lists such as 1. First followed by - Separate therefore turns the bullet into another numbered item instead of preserving two distinct lists.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/workspace/document-conversion.ts
Line: 197-214

Comment:
**Mixed lists lose structure**

The parser chooses the list type from the first marker but then continues consuming both numbered and bullet markers. Importing adjacent mixed lists such as `1. First` followed by `- Separate` therefore turns the bullet into another numbered item instead of preserving two distinct lists.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Cursor Fix in Claude Code

Comment on lines +8 to +11
TRIAL_ORIGIN="https://codex-remote.tailab0ab3.ts.net:18443"
APP_PORT=18480
COLLABORATION_PORT=12345
SERVE_PORT=18443

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Trial boundary is undocumented

This change introduces a Tailscale-served origin, three dedicated ports, and a new Nginx runtime boundary. The repository directive requires the owning focused documentation to be updated whenever commands, ports, workflows, or runtime boundaries change. That required documentation must be added before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: .superset/workspace-trial.sh
Line: 8-11

Comment:
**Trial boundary is undocumented**

This change introduces a Tailscale-served origin, three dedicated ports, and a new Nginx runtime boundary. The repository directive requires the owning focused documentation to be updated whenever commands, ports, workflows, or runtime boundaries change. That required documentation must be added before merging.

**Context Used:** AGENTS.md ([source](https://github.com/programcode/debatelab/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Cursor Fix in Claude Code

@greptile-apps

greptile-apps Bot commented Sep 23, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P2 Writing Failures Stay Hidden src/pages/LibraryPage.tsx:1557 ▶

    Both the new delete operation and the save operation use success-only Promise chains. If either database request fails because of connectivity or policy enforcement, the page produces an unhandled rejection without explaining what happened, leaving the displayed item state ambiguous.

@Programcode

Copy link
Copy Markdown
Owner Author

Closing per Adam, not merging.

This branch was successfully deployed

1 active deployment
Preview — d52598cc Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants