-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Add a share button when logged into cloud #4448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import axios from "axios" | ||
| import * as vscode from "vscode" | ||
|
|
||
| import type { ShareResponse } from "@roo-code/types" | ||
| import { getRooCodeApiUrl } from "./Config" | ||
| import type { AuthService } from "./AuthService" | ||
| import type { SettingsService } from "./SettingsService" | ||
| import { getUserAgent } from "./utils" | ||
|
|
||
| export class ShareService { | ||
| private authService: AuthService | ||
| private settingsService: SettingsService | ||
| private log: (...args: unknown[]) => void | ||
|
|
||
| constructor(authService: AuthService, settingsService: SettingsService, log?: (...args: unknown[]) => void) { | ||
| this.authService = authService | ||
| this.settingsService = settingsService | ||
| this.log = log || console.log | ||
| } | ||
|
|
||
| /** | ||
| * Share a task: Create link and copy to clipboard | ||
| * Returns true if successful, false if failed | ||
| */ | ||
| async shareTask(taskId: string): Promise<boolean> { | ||
| try { | ||
| if (!this.authService.hasActiveSession()) { | ||
| return false | ||
| } | ||
|
|
||
| const sessionToken = this.authService.getSessionToken() | ||
| if (!sessionToken) { | ||
| return false | ||
| } | ||
|
|
||
| const response = await axios.post( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might consider using Main motivation (other than not using a library unless we have to): as far as I can tell you can do a better job of setting timeouts with fetch than axios, which is something we should start doing for most of these requests (another cleanup task I haven't gotten to yet). Hopefully those will be better examples for Roo too, although maybe that's too optimistic. That would mean you'd need to explicitly check the status & fail on non-2xx, since that behavior's different between fetch and axios.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of changing all of the axioses to fetches as a follow up and adding a rules file with instructions to use fetch instead going forward. |
||
| `${getRooCodeApiUrl()}/api/extension/share`, | ||
| { taskId }, | ||
| { | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${sessionToken}`, | ||
| "User-Agent": getUserAgent(), | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
| const data = response.data as ShareResponse | ||
mrubens marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this.log("[share] Share link created successfully:", data) | ||
|
|
||
| if (data.success && data.shareUrl) { | ||
| // Copy to clipboard | ||
| await vscode.env.clipboard.writeText(data.shareUrl) | ||
| return true | ||
| } else { | ||
| this.log("[share] Share failed:", data.error) | ||
| return false | ||
| } | ||
| } catch (error) { | ||
| this.log("[share] Error sharing task:", error) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if sharing is available | ||
| */ | ||
| async canShareTask(): Promise<boolean> { | ||
mrubens marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| if (!this.authService.isAuthenticated()) { | ||
| return false | ||
| } | ||
|
|
||
| return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing | ||
| } catch (error) { | ||
| this.log("[share] Error checking if task can be shared:", error) | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import { describe, it, expect, beforeEach, vi, type MockedFunction } from "vitest" | ||
| import axios from "axios" | ||
| import * as vscode from "vscode" | ||
|
|
||
| import { ShareService } from "../ShareService" | ||
| import type { AuthService } from "../AuthService" | ||
| import type { SettingsService } from "../SettingsService" | ||
|
|
||
| // Mock axios | ||
| vi.mock("axios") | ||
| const mockedAxios = axios as any | ||
|
|
||
| // Mock vscode | ||
| vi.mock("vscode", () => ({ | ||
| window: { | ||
| showInformationMessage: vi.fn(), | ||
| showErrorMessage: vi.fn(), | ||
| }, | ||
| env: { | ||
| clipboard: { | ||
| writeText: vi.fn(), | ||
| }, | ||
| openExternal: vi.fn(), | ||
| }, | ||
| Uri: { | ||
| parse: vi.fn(), | ||
| }, | ||
| extensions: { | ||
| getExtension: vi.fn(() => ({ | ||
| packageJSON: { version: "1.0.0" }, | ||
| })), | ||
| }, | ||
| })) | ||
|
|
||
| // Mock config | ||
| vi.mock("../Config", () => ({ | ||
| getRooCodeApiUrl: () => "https://app.roocode.com", | ||
| })) | ||
|
|
||
| // Mock utils | ||
| vi.mock("../utils", () => ({ | ||
| getUserAgent: () => "Roo-Code 1.0.0", | ||
| })) | ||
|
|
||
| describe("ShareService", () => { | ||
| let shareService: ShareService | ||
| let mockAuthService: AuthService | ||
| let mockSettingsService: SettingsService | ||
| let mockLog: MockedFunction<(...args: unknown[]) => void> | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks() | ||
|
|
||
| mockLog = vi.fn() | ||
| mockAuthService = { | ||
| hasActiveSession: vi.fn(), | ||
| getSessionToken: vi.fn(), | ||
| isAuthenticated: vi.fn(), | ||
| } as any | ||
|
|
||
| mockSettingsService = { | ||
| getSettings: vi.fn(), | ||
| } as any | ||
|
|
||
| shareService = new ShareService(mockAuthService, mockSettingsService, mockLog) | ||
| }) | ||
|
|
||
| describe("shareTask", () => { | ||
| it("should share task and copy to clipboard", async () => { | ||
| const mockResponse = { | ||
| data: { | ||
| success: true, | ||
| shareUrl: "https://app.roocode.com/share/abc123", | ||
| }, | ||
| } | ||
|
|
||
| ;(mockAuthService.hasActiveSession as any).mockReturnValue(true) | ||
| ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") | ||
| mockedAxios.post.mockResolvedValue(mockResponse) | ||
|
|
||
| const result = await shareService.shareTask("task-123") | ||
|
|
||
| expect(result).toBe(true) | ||
| expect(mockedAxios.post).toHaveBeenCalledWith( | ||
| "https://app.roocode.com/api/extension/share", | ||
| { taskId: "task-123" }, | ||
| { | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: "Bearer session-token", | ||
| "User-Agent": "Roo-Code 1.0.0", | ||
| }, | ||
| }, | ||
| ) | ||
| expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123") | ||
| }) | ||
|
|
||
| it("should handle API error response", async () => { | ||
| const mockResponse = { | ||
| data: { | ||
| success: false, | ||
| error: "Task not found", | ||
| }, | ||
| } | ||
|
|
||
| ;(mockAuthService.hasActiveSession as any).mockReturnValue(true) | ||
| ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") | ||
| mockedAxios.post.mockResolvedValue(mockResponse) | ||
|
|
||
| const result = await shareService.shareTask("task-123") | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should handle authentication errors", async () => { | ||
| ;(mockAuthService.hasActiveSession as any).mockReturnValue(false) | ||
|
|
||
| const result = await shareService.shareTask("task-123") | ||
|
|
||
| expect(result).toBe(false) | ||
| expect(mockedAxios.post).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it("should handle 403 error for disabled sharing", async () => { | ||
| ;(mockAuthService.hasActiveSession as any).mockReturnValue(true) | ||
| ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") | ||
|
|
||
| const error = { | ||
| isAxiosError: true, | ||
| response: { | ||
| status: 403, | ||
| data: { | ||
| error: "Task sharing is not enabled for this organization", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| mockedAxios.isAxiosError.mockReturnValue(true) | ||
| mockedAxios.post.mockRejectedValue(error) | ||
|
|
||
| const result = await shareService.shareTask("task-123") | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should handle unexpected errors", async () => { | ||
| ;(mockAuthService.hasActiveSession as any).mockReturnValue(true) | ||
| ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") | ||
|
|
||
| mockedAxios.post.mockRejectedValue(new Error("Network error")) | ||
|
|
||
| const result = await shareService.shareTask("task-123") | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe("canShareTask", () => { | ||
| it("should return true when authenticated and sharing is enabled", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) | ||
| ;(mockSettingsService.getSettings as any).mockReturnValue({ | ||
| cloudSettings: { | ||
| enableTaskSharing: true, | ||
| }, | ||
| }) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return false when authenticated but sharing is disabled", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) | ||
| ;(mockSettingsService.getSettings as any).mockReturnValue({ | ||
| cloudSettings: { | ||
| enableTaskSharing: false, | ||
| }, | ||
| }) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return false when authenticated and sharing setting is undefined (default)", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) | ||
| ;(mockSettingsService.getSettings as any).mockReturnValue({ | ||
| cloudSettings: {}, | ||
| }) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return false when authenticated and no settings available (default)", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) | ||
| ;(mockSettingsService.getSettings as any).mockReturnValue(undefined) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return false when not authenticated", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockReturnValue(false) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should handle errors gracefully", async () => { | ||
| ;(mockAuthService.isAuthenticated as any).mockImplementation(() => { | ||
| throw new Error("Auth error") | ||
| }) | ||
|
|
||
| const result = await shareService.canShareTask() | ||
|
|
||
| expect(result).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import * as vscode from "vscode" | ||
|
|
||
| /** | ||
| * Get the User-Agent string for API requests | ||
| * @param context Optional extension context for more accurate version detection | ||
| * @returns User-Agent string in format "Roo-Code {version}" | ||
| */ | ||
| export function getUserAgent(context?: vscode.ExtensionContext): string { | ||
| return `Roo-Code ${context?.extension?.packageJSON?.version || "unknown"}` | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.