Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ export class CloudService {

// ShareService

public async shareTask(taskId: string): Promise<boolean> {
public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") {
this.ensureInitialized()
return this.shareService!.shareTask(taskId)
return this.shareService!.shareTask(taskId, visibility)
}

public async canShareTask(): Promise<boolean> {
Expand Down
20 changes: 10 additions & 10 deletions packages/cloud/src/ShareService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { AuthService } from "./AuthService"
import type { SettingsService } from "./SettingsService"
import { getUserAgent } from "./utils"

export type ShareVisibility = "organization" | "public"

export class ShareService {
private authService: AuthService
private settingsService: SettingsService
Expand All @@ -19,19 +21,19 @@ export class ShareService {
}

/**
* Share a task: Create link and copy to clipboard
* Returns true if successful, false if failed
* Share a task with specified visibility
* Returns the share response data
*/
async shareTask(taskId: string): Promise<boolean> {
async shareTask(taskId: string, visibility: ShareVisibility = "organization") {
try {
const sessionToken = this.authService.getSessionToken()
if (!sessionToken) {
return false
throw new Error("Authentication required")
}

const response = await axios.post(
`${getRooCodeApiUrl()}/api/extension/share`,
{ taskId },
{ taskId, visibility },
{
headers: {
"Content-Type": "application/json",
Expand All @@ -47,14 +49,12 @@ export class ShareService {
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
}

return data
} catch (error) {
this.log("[share] Error sharing task:", error)
return false
throw error
}
}

Expand Down
84 changes: 49 additions & 35 deletions packages/cloud/src/__tests__/ShareService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ vi.mock("vscode", () => ({
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
showQuickPick: vi.fn(),
},
env: {
clipboard: {
Expand Down Expand Up @@ -68,24 +69,24 @@ describe("ShareService", () => {
})

describe("shareTask", () => {
it("should share task and copy to clipboard", async () => {
it("should share task with organization visibility 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")
const result = await shareService.shareTask("task-123", "organization")

expect(result).toBe(true)
expect(result.success).toBe(true)
expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123")
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123" },
{ taskId: "task-123", visibility: "organization" },
{
headers: {
"Content-Type": "application/json",
Expand All @@ -97,63 +98,76 @@ describe("ShareService", () => {
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123")
})

it("should handle API error response", async () => {
it("should share task with public visibility", async () => {
const mockResponse = {
data: {
success: false,
error: "Task not found",
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")
const result = await shareService.shareTask("task-123", "public")

expect(result).toBe(false)
expect(result.success).toBe(true)
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123", visibility: "public" },
expect.any(Object),
)
})

it("should handle authentication errors", async () => {
;(mockAuthService.hasActiveSession as any).mockReturnValue(false)
it("should default to organization visibility when not specified", async () => {
const mockResponse = {
data: {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
},
}

;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)

const result = await shareService.shareTask("task-123")

expect(result).toBe(false)
expect(mockedAxios.post).not.toHaveBeenCalled()
expect(result.success).toBe(true)
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123", visibility: "organization" },
expect.any(Object),
)
})

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",
},
it("should handle API error response", async () => {
const mockResponse = {
data: {
success: false,
error: "Task not found",
},
}

mockedAxios.isAxiosError.mockReturnValue(true)
mockedAxios.post.mockRejectedValue(error)
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)

const result = await shareService.shareTask("task-123")
const result = await shareService.shareTask("task-123", "organization")

expect(result).toBe(false)
expect(result.success).toBe(false)
expect(result.error).toBe("Task not found")
})

it("should handle authentication errors", async () => {
;(mockAuthService.getSessionToken as any).mockReturnValue(null)

await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required")
})

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)
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error")
})
})

Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export const shareResponseSchema = z.object({
success: z.boolean(),
shareUrl: z.string().optional(),
error: z.string().optional(),
isNewShare: z.boolean().optional(),
manageUrl: z.string().optional(),
})

export type ShareResponse = z.infer<typeof shareResponseSchema>
29 changes: 22 additions & 7 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,31 @@ export const webviewMessageHandler = async (
}

try {
const success = await CloudService.instance.shareTask(shareTaskId)
if (success) {
// Show success message
vscode.window.showInformationMessage(t("common:info.share_link_copied"))
const visibility = message.visibility || "organization"
const result = await CloudService.instance.shareTask(shareTaskId, visibility)

if (result.success && result.shareUrl) {
// Show success notification
const messageKey =
visibility === "public"
? "common:info.public_share_link_copied"
: "common:info.organization_share_link_copied"
vscode.window.showInformationMessage(t(messageKey))
} else {
// Show generic failure message
vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
// Handle error
const errorMessage = result.error || "Failed to create share link"
if (errorMessage.includes("Authentication")) {
vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
} else if (errorMessage.includes("sharing is not enabled")) {
vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
} else if (errorMessage.includes("not found")) {
vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
} else {
vscode.window.showErrorMessage(errorMessage)
}
}
} catch (error) {
// Show generic failure message
provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
}
break
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/ca/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@
"condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid",
"condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent",
"share_task_failed": "Ha fallat compartir la tasca. Si us plau, torna-ho a provar.",
"share_no_active_task": "No hi ha cap tasca activa per compartir"
"share_no_active_task": "No hi ha cap tasca activa per compartir",
"share_auth_required": "Es requereix autenticació. Si us plau, inicia sessió per compartir tasques.",
"share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.",
"share_task_not_found": "Tasca no trobada o accés denegat."
},
"warnings": {
"no_terminal_content": "No s'ha seleccionat contingut de terminal",
Expand All @@ -78,7 +81,9 @@
"settings_imported": "Configuració importada correctament.",
"share_link_copied": "Enllaç de compartició copiat al portapapers",
"image_copied_to_clipboard": "URI de dades de la imatge copiada al portapapers",
"image_saved": "Imatge desada a {{path}}"
"image_saved": "Imatge desada a {{path}}",
"organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!",
"public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!"
},
"answers": {
"yes": "Sí",
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
"condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig",
"condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen",
"share_task_failed": "Teilen der Aufgabe fehlgeschlagen. Bitte versuche es erneut.",
"share_no_active_task": "Keine aktive Aufgabe zum Teilen"
"share_no_active_task": "Keine aktive Aufgabe zum Teilen",
"share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.",
"share_not_enabled": "Aufgabenfreigabe ist für diese Organisation nicht aktiviert.",
"share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert."
},
"warnings": {
"no_terminal_content": "Kein Terminal-Inhalt ausgewählt",
Expand All @@ -74,7 +77,9 @@
"settings_imported": "Einstellungen erfolgreich importiert.",
"share_link_copied": "Share-Link in die Zwischenablage kopiert",
"image_copied_to_clipboard": "Bild-Daten-URI in die Zwischenablage kopiert",
"image_saved": "Bild gespeichert unter {{path}}"
"image_saved": "Bild gespeichert unter {{path}}",
"organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!",
"public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!"
},
"answers": {
"yes": "Ja",
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
"condense_handler_invalid": "API handler for condensing context is invalid",
"condense_context_grew": "Context size increased during condensing; skipping this attempt",
"share_task_failed": "Failed to share task. Please try again.",
"share_no_active_task": "No active task to share"
"share_no_active_task": "No active task to share",
"share_auth_required": "Authentication required. Please sign in to share tasks.",
"share_not_enabled": "Task sharing is not enabled for this organization.",
"share_task_not_found": "Task not found or access denied."
},
"warnings": {
"no_terminal_content": "No terminal content selected",
Expand All @@ -73,6 +76,8 @@
"default_storage_path": "Reverted to using default storage path",
"settings_imported": "Settings imported successfully.",
"share_link_copied": "Share link copied to clipboard",
"organization_share_link_copied": "Organization share link copied to clipboard!",
"public_share_link_copied": "Public share link copied to clipboard!",
"image_copied_to_clipboard": "Image data URI copied to clipboard",
"image_saved": "Image saved to {{path}}"
},
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
"condense_handler_invalid": "El manejador de API para condensar el contexto no es válido",
"condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento",
"share_task_failed": "Error al compartir la tarea. Por favor, inténtalo de nuevo.",
"share_no_active_task": "No hay tarea activa para compartir"
"share_no_active_task": "No hay tarea activa para compartir",
"share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.",
"share_not_enabled": "La compartición de tareas no está habilitada para esta organización.",
"share_task_not_found": "Tarea no encontrada o acceso denegado."
},
"warnings": {
"no_terminal_content": "No hay contenido de terminal seleccionado",
Expand All @@ -74,7 +77,9 @@
"settings_imported": "Configuración importada correctamente.",
"share_link_copied": "Enlace de compartir copiado al portapapeles",
"image_copied_to_clipboard": "URI de datos de imagen copiada al portapapeles",
"image_saved": "Imagen guardada en {{path}}"
"image_saved": "Imagen guardada en {{path}}",
"organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!",
"public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!"
},
"answers": {
"yes": "Sí",
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
"condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide",
"condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée",
"share_task_failed": "Échec du partage de la tâche. Veuillez réessayer.",
"share_no_active_task": "Aucune tâche active à partager"
"share_no_active_task": "Aucune tâche active à partager",
"share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.",
"share_not_enabled": "Le partage de tâches n'est pas activé pour cette organisation.",
"share_task_not_found": "Tâche non trouvée ou accès refusé."
},
"warnings": {
"no_terminal_content": "Aucun contenu de terminal sélectionné",
Expand All @@ -74,7 +77,9 @@
"settings_imported": "Paramètres importés avec succès.",
"share_link_copied": "Lien de partage copié dans le presse-papiers",
"image_copied_to_clipboard": "URI de données d'image copiée dans le presse-papiers",
"image_saved": "Image enregistrée dans {{path}}"
"image_saved": "Image enregistrée dans {{path}}",
"organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !",
"public_share_link_copied": "Lien de partage public copié dans le presse-papiers !"
},
"answers": {
"yes": "Oui",
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/hi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
"condense_handler_invalid": "संदर्भ को संक्षिप्त करने के लिए API हैंडलर अमान्य है",
"condense_context_grew": "संक्षिप्तीकरण के दौरान संदर्भ का आकार बढ़ गया; इस प्रयास को छोड़ा जा रहा है",
"share_task_failed": "कार्य साझा करने में विफल। कृपया पुनः प्रयास करें।",
"share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं"
"share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं",
"share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।",
"share_not_enabled": "इस संगठन के लिए कार्य साझाकरण सक्षम नहीं है।",
"share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।"
},
"warnings": {
"no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं",
Expand All @@ -74,7 +77,9 @@
"settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।",
"share_link_copied": "साझा लिंक क्लिपबोर्ड पर कॉपी किया गया",
"image_copied_to_clipboard": "छवि डेटा URI क्लिपबोर्ड में कॉपी की गई",
"image_saved": "छवि {{path}} में सहेजी गई"
"image_saved": "छवि {{path}} में सहेजी गई",
"organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
"public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!"
},
"answers": {
"yes": "हां",
Expand Down
Loading
Loading