Skip to content

Commit b6bbb1e

Browse files
authored
Add choice to share with organization or publicly (#4899)
1 parent 9e5ecb1 commit b6bbb1e

File tree

43 files changed

+364
-113
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+364
-113
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ export class CloudService {
138138

139139
// ShareService
140140

141-
public async shareTask(taskId: string): Promise<boolean> {
141+
public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") {
142142
this.ensureInitialized()
143-
return this.shareService!.shareTask(taskId)
143+
return this.shareService!.shareTask(taskId, visibility)
144144
}
145145

146146
public async canShareTask(): Promise<boolean> {

packages/cloud/src/ShareService.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { AuthService } from "./AuthService"
77
import type { SettingsService } from "./SettingsService"
88
import { getUserAgent } from "./utils"
99

10+
export type ShareVisibility = "organization" | "public"
11+
1012
export class ShareService {
1113
private authService: AuthService
1214
private settingsService: SettingsService
@@ -19,19 +21,19 @@ export class ShareService {
1921
}
2022

2123
/**
22-
* Share a task: Create link and copy to clipboard
23-
* Returns true if successful, false if failed
24+
* Share a task with specified visibility
25+
* Returns the share response data
2426
*/
25-
async shareTask(taskId: string): Promise<boolean> {
27+
async shareTask(taskId: string, visibility: ShareVisibility = "organization") {
2628
try {
2729
const sessionToken = this.authService.getSessionToken()
2830
if (!sessionToken) {
29-
return false
31+
throw new Error("Authentication required")
3032
}
3133

3234
const response = await axios.post(
3335
`${getRooCodeApiUrl()}/api/extension/share`,
34-
{ taskId },
36+
{ taskId, visibility },
3537
{
3638
headers: {
3739
"Content-Type": "application/json",
@@ -47,14 +49,12 @@ export class ShareService {
4749
if (data.success && data.shareUrl) {
4850
// Copy to clipboard
4951
await vscode.env.clipboard.writeText(data.shareUrl)
50-
return true
51-
} else {
52-
this.log("[share] Share failed:", data.error)
53-
return false
5452
}
53+
54+
return data
5555
} catch (error) {
5656
this.log("[share] Error sharing task:", error)
57-
return false
57+
throw error
5858
}
5959
}
6060

packages/cloud/src/__tests__/ShareService.test.ts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vi.mock("vscode", () => ({
1717
window: {
1818
showInformationMessage: vi.fn(),
1919
showErrorMessage: vi.fn(),
20+
showQuickPick: vi.fn(),
2021
},
2122
env: {
2223
clipboard: {
@@ -68,24 +69,24 @@ describe("ShareService", () => {
6869
})
6970

7071
describe("shareTask", () => {
71-
it("should share task and copy to clipboard", async () => {
72+
it("should share task with organization visibility and copy to clipboard", async () => {
7273
const mockResponse = {
7374
data: {
7475
success: true,
7576
shareUrl: "https://app.roocode.com/share/abc123",
7677
},
7778
}
7879

79-
;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
8080
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
8181
mockedAxios.post.mockResolvedValue(mockResponse)
8282

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

85-
expect(result).toBe(true)
85+
expect(result.success).toBe(true)
86+
expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123")
8687
expect(mockedAxios.post).toHaveBeenCalledWith(
8788
"https://app.roocode.com/api/extension/share",
88-
{ taskId: "task-123" },
89+
{ taskId: "task-123", visibility: "organization" },
8990
{
9091
headers: {
9192
"Content-Type": "application/json",
@@ -97,63 +98,76 @@ describe("ShareService", () => {
9798
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123")
9899
})
99100

100-
it("should handle API error response", async () => {
101+
it("should share task with public visibility", async () => {
101102
const mockResponse = {
102103
data: {
103-
success: false,
104-
error: "Task not found",
104+
success: true,
105+
shareUrl: "https://app.roocode.com/share/abc123",
105106
},
106107
}
107108

108-
;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
109109
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
110110
mockedAxios.post.mockResolvedValue(mockResponse)
111111

112-
const result = await shareService.shareTask("task-123")
112+
const result = await shareService.shareTask("task-123", "public")
113113

114-
expect(result).toBe(false)
114+
expect(result.success).toBe(true)
115+
expect(mockedAxios.post).toHaveBeenCalledWith(
116+
"https://app.roocode.com/api/extension/share",
117+
{ taskId: "task-123", visibility: "public" },
118+
expect.any(Object),
119+
)
115120
})
116121

117-
it("should handle authentication errors", async () => {
118-
;(mockAuthService.hasActiveSession as any).mockReturnValue(false)
122+
it("should default to organization visibility when not specified", async () => {
123+
const mockResponse = {
124+
data: {
125+
success: true,
126+
shareUrl: "https://app.roocode.com/share/abc123",
127+
},
128+
}
129+
130+
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
131+
mockedAxios.post.mockResolvedValue(mockResponse)
119132

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

122-
expect(result).toBe(false)
123-
expect(mockedAxios.post).not.toHaveBeenCalled()
135+
expect(result.success).toBe(true)
136+
expect(mockedAxios.post).toHaveBeenCalledWith(
137+
"https://app.roocode.com/api/extension/share",
138+
{ taskId: "task-123", visibility: "organization" },
139+
expect.any(Object),
140+
)
124141
})
125142

126-
it("should handle 403 error for disabled sharing", async () => {
127-
;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
128-
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
129-
130-
const error = {
131-
isAxiosError: true,
132-
response: {
133-
status: 403,
134-
data: {
135-
error: "Task sharing is not enabled for this organization",
136-
},
143+
it("should handle API error response", async () => {
144+
const mockResponse = {
145+
data: {
146+
success: false,
147+
error: "Task not found",
137148
},
138149
}
139150

140-
mockedAxios.isAxiosError.mockReturnValue(true)
141-
mockedAxios.post.mockRejectedValue(error)
151+
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
152+
mockedAxios.post.mockResolvedValue(mockResponse)
142153

143-
const result = await shareService.shareTask("task-123")
154+
const result = await shareService.shareTask("task-123", "organization")
144155

145-
expect(result).toBe(false)
156+
expect(result.success).toBe(false)
157+
expect(result.error).toBe("Task not found")
158+
})
159+
160+
it("should handle authentication errors", async () => {
161+
;(mockAuthService.getSessionToken as any).mockReturnValue(null)
162+
163+
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required")
146164
})
147165

148166
it("should handle unexpected errors", async () => {
149-
;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
150167
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
151-
152168
mockedAxios.post.mockRejectedValue(new Error("Network error"))
153169

154-
const result = await shareService.shareTask("task-123")
155-
156-
expect(result).toBe(false)
170+
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error")
157171
})
158172
})
159173

packages/types/src/cloud.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export const shareResponseSchema = z.object({
139139
success: z.boolean(),
140140
shareUrl: z.string().optional(),
141141
error: z.string().optional(),
142+
isNewShare: z.boolean().optional(),
143+
manageUrl: z.string().optional(),
142144
})
143145

144146
export type ShareResponse = z.infer<typeof shareResponseSchema>

src/core/webview/webviewMessageHandler.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,31 @@ export const webviewMessageHandler = async (
233233
}
234234

235235
try {
236-
const success = await CloudService.instance.shareTask(shareTaskId)
237-
if (success) {
238-
// Show success message
239-
vscode.window.showInformationMessage(t("common:info.share_link_copied"))
236+
const visibility = message.visibility || "organization"
237+
const result = await CloudService.instance.shareTask(shareTaskId, visibility)
238+
239+
if (result.success && result.shareUrl) {
240+
// Show success notification
241+
const messageKey =
242+
visibility === "public"
243+
? "common:info.public_share_link_copied"
244+
: "common:info.organization_share_link_copied"
245+
vscode.window.showInformationMessage(t(messageKey))
240246
} else {
241-
// Show generic failure message
242-
vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
247+
// Handle error
248+
const errorMessage = result.error || "Failed to create share link"
249+
if (errorMessage.includes("Authentication")) {
250+
vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
251+
} else if (errorMessage.includes("sharing is not enabled")) {
252+
vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
253+
} else if (errorMessage.includes("not found")) {
254+
vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
255+
} else {
256+
vscode.window.showErrorMessage(errorMessage)
257+
}
243258
}
244259
} catch (error) {
245-
// Show generic failure message
260+
provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
246261
vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
247262
}
248263
break

src/i18n/locales/ca/common.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@
6363
"condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid",
6464
"condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent",
6565
"share_task_failed": "Ha fallat compartir la tasca. Si us plau, torna-ho a provar.",
66-
"share_no_active_task": "No hi ha cap tasca activa per compartir"
66+
"share_no_active_task": "No hi ha cap tasca activa per compartir",
67+
"share_auth_required": "Es requereix autenticació. Si us plau, inicia sessió per compartir tasques.",
68+
"share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.",
69+
"share_task_not_found": "Tasca no trobada o accés denegat."
6770
},
6871
"warnings": {
6972
"no_terminal_content": "No s'ha seleccionat contingut de terminal",
@@ -78,7 +81,9 @@
7881
"settings_imported": "Configuració importada correctament.",
7982
"share_link_copied": "Enllaç de compartició copiat al portapapers",
8083
"image_copied_to_clipboard": "URI de dades de la imatge copiada al portapapers",
81-
"image_saved": "Imatge desada a {{path}}"
84+
"image_saved": "Imatge desada a {{path}}",
85+
"organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!",
86+
"public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!"
8287
},
8388
"answers": {
8489
"yes": "",

src/i18n/locales/de/common.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig",
6060
"condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen",
6161
"share_task_failed": "Teilen der Aufgabe fehlgeschlagen. Bitte versuche es erneut.",
62-
"share_no_active_task": "Keine aktive Aufgabe zum Teilen"
62+
"share_no_active_task": "Keine aktive Aufgabe zum Teilen",
63+
"share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.",
64+
"share_not_enabled": "Aufgabenfreigabe ist für diese Organisation nicht aktiviert.",
65+
"share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert."
6366
},
6467
"warnings": {
6568
"no_terminal_content": "Kein Terminal-Inhalt ausgewählt",
@@ -74,7 +77,9 @@
7477
"settings_imported": "Einstellungen erfolgreich importiert.",
7578
"share_link_copied": "Share-Link in die Zwischenablage kopiert",
7679
"image_copied_to_clipboard": "Bild-Daten-URI in die Zwischenablage kopiert",
77-
"image_saved": "Bild gespeichert unter {{path}}"
80+
"image_saved": "Bild gespeichert unter {{path}}",
81+
"organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!",
82+
"public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!"
7883
},
7984
"answers": {
8085
"yes": "Ja",

src/i18n/locales/en/common.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"condense_handler_invalid": "API handler for condensing context is invalid",
6060
"condense_context_grew": "Context size increased during condensing; skipping this attempt",
6161
"share_task_failed": "Failed to share task. Please try again.",
62-
"share_no_active_task": "No active task to share"
62+
"share_no_active_task": "No active task to share",
63+
"share_auth_required": "Authentication required. Please sign in to share tasks.",
64+
"share_not_enabled": "Task sharing is not enabled for this organization.",
65+
"share_task_not_found": "Task not found or access denied."
6366
},
6467
"warnings": {
6568
"no_terminal_content": "No terminal content selected",
@@ -73,6 +76,8 @@
7376
"default_storage_path": "Reverted to using default storage path",
7477
"settings_imported": "Settings imported successfully.",
7578
"share_link_copied": "Share link copied to clipboard",
79+
"organization_share_link_copied": "Organization share link copied to clipboard!",
80+
"public_share_link_copied": "Public share link copied to clipboard!",
7681
"image_copied_to_clipboard": "Image data URI copied to clipboard",
7782
"image_saved": "Image saved to {{path}}"
7883
},

src/i18n/locales/es/common.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"condense_handler_invalid": "El manejador de API para condensar el contexto no es válido",
6060
"condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento",
6161
"share_task_failed": "Error al compartir la tarea. Por favor, inténtalo de nuevo.",
62-
"share_no_active_task": "No hay tarea activa para compartir"
62+
"share_no_active_task": "No hay tarea activa para compartir",
63+
"share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.",
64+
"share_not_enabled": "La compartición de tareas no está habilitada para esta organización.",
65+
"share_task_not_found": "Tarea no encontrada o acceso denegado."
6366
},
6467
"warnings": {
6568
"no_terminal_content": "No hay contenido de terminal seleccionado",
@@ -74,7 +77,9 @@
7477
"settings_imported": "Configuración importada correctamente.",
7578
"share_link_copied": "Enlace de compartir copiado al portapapeles",
7679
"image_copied_to_clipboard": "URI de datos de imagen copiada al portapapeles",
77-
"image_saved": "Imagen guardada en {{path}}"
80+
"image_saved": "Imagen guardada en {{path}}",
81+
"organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!",
82+
"public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!"
7883
},
7984
"answers": {
8085
"yes": "",

src/i18n/locales/fr/common.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide",
6060
"condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée",
6161
"share_task_failed": "Échec du partage de la tâche. Veuillez réessayer.",
62-
"share_no_active_task": "Aucune tâche active à partager"
62+
"share_no_active_task": "Aucune tâche active à partager",
63+
"share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.",
64+
"share_not_enabled": "Le partage de tâches n'est pas activé pour cette organisation.",
65+
"share_task_not_found": "Tâche non trouvée ou accès refusé."
6366
},
6467
"warnings": {
6568
"no_terminal_content": "Aucun contenu de terminal sélectionné",
@@ -74,7 +77,9 @@
7477
"settings_imported": "Paramètres importés avec succès.",
7578
"share_link_copied": "Lien de partage copié dans le presse-papiers",
7679
"image_copied_to_clipboard": "URI de données d'image copiée dans le presse-papiers",
77-
"image_saved": "Image enregistrée dans {{path}}"
80+
"image_saved": "Image enregistrée dans {{path}}",
81+
"organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !",
82+
"public_share_link_copied": "Lien de partage public copié dans le presse-papiers !"
7883
},
7984
"answers": {
8085
"yes": "Oui",

0 commit comments

Comments
 (0)