fix(cli): route share links through Kilo session API (#10736)

* fix(cli): route share links through Kilo session API

Refactor shareSession and unshareSession to return Effect values using
EffectBridge.fromPromise and replace the ShareNext dependency in
SessionShare with KiloSession calls so CLI-generated share URLs point
to app.kilo.ai/s/ public links.

* test(session-share): replace EffectBridge usage with direct Storage service calls

Update session-share test to use Storage.Service directly instead of
wrapping Storage.write in EffectBridge.fromPromise, aligning test code
with the refactored service-based architecture. Add Storage.defaultLayer
to the test effect layer composition.
This commit is contained in:
Imanol Maiztegui
2026-05-29 18:15:42 +02:00
committed by GitHub
parent 979cb7e2ab
commit 57bc6eea58
4 changed files with 83 additions and 12 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Use Kilo session share links when sharing conversations from the CLI.
@@ -1,8 +1,9 @@
// kilocode_change - new file
import { remapChildren as _remapChildren } from "./fork"
import z from "zod"
import { Effect, Schema } from "effect"
import { Cause, Effect, Schema } from "effect"
import { BusEvent } from "@/bus/bus-event"
import { EffectBridge } from "@/effect/bridge"
import { Session } from "@/session/session"
import { MessageID, SessionID } from "@/session/schema"
import { fn } from "@/util/fn"
@@ -230,14 +231,18 @@ export namespace KiloSession {
// Session lifecycle hooks (share, unshare, remove)
// ---------------------------------------------------------------------------
export async function shareSession(id: string): Promise<{ url: string }> {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
return KiloSessions.share(id)
export function shareSession(id: SessionID) {
return EffectBridge.fromPromise(async () => {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
return KiloSessions.share(id)
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
}
export async function unshareSession(id: string): Promise<void> {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
await KiloSessions.unshare(id)
export function unshareSession(id: SessionID) {
return EffectBridge.fromPromise(async () => {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
await KiloSessions.unshare(id)
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
}
export async function removeSession(id: string): Promise<void> {
+3 -5
View File
@@ -4,7 +4,7 @@ import { SyncEvent } from "@/sync"
import { Effect, Layer, Scope, Context } from "effect"
import { Config } from "@/config/config"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as ShareNext from "./share-next"
import { KiloSession } from "@/kilocode/session" // kilocode_change
export interface Interface {
readonly create: (input?: Session.CreateInput) => Effect.Effect<Session.Info>
@@ -19,20 +19,19 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const cfg = yield* Config.Service
const session = yield* Session.Service
const shareNext = yield* ShareNext.Service
const scope = yield* Scope.Scope
const sync = yield* SyncEvent.Service
const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
const conf = yield* cfg.get()
if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
const result = yield* shareNext.create(sessionID)
const result = yield* KiloSession.shareSession(sessionID) // kilocode_change - use Kilo public share URLs
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } })
return result
})
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
yield* shareNext.remove(sessionID)
yield* KiloSession.unshareSession(sessionID) // kilocode_change - use Kilo public share URLs
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })
})
@@ -50,7 +49,6 @@ export const layer = Layer.effect(
)
export const defaultLayer = layer.pipe(
Layer.provide(ShareNext.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
@@ -0,0 +1,63 @@
import { expect, spyOn } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { Session } from "../../src/session/session"
import { SessionShare } from "../../src/share/session"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Auth.defaultLayer, Storage.defaultLayer, CrossSpawnSpawner.defaultLayer))
const layer = SessionShare.layer.pipe(
Layer.provideMerge(Session.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
)
it.instance("shares and unshares sessions through Kilo public URLs", () => {
const urls: string[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL) => {
const url = String(input)
urls.push(url)
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/share")) return Response.json({ public_id: "public-1" })
if (url.endsWith("/unshare")) return new Response(null, { status: 200 })
return new Response("{}", { status: 200 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
return Effect.gen(function* () {
const auth = yield* Auth.Service
const share = yield* SessionShare.Service
const session = yield* Session.Service
const storage = yield* Storage.Service
yield* auth.set("kilo", { type: "api", key: "test-token" })
const info = yield* share.create({ title: "share-test" })
yield* storage.write(["session_share", info.id], { id: "remote-1", ingestPath: "/api/ingest/session-1" })
const result = yield* share.share(info.id)
expect(result.url).toBe("https://app.kilo.ai/s/public-1")
expect((yield* session.get(info.id)).share?.url).toBe("https://app.kilo.ai/s/public-1")
yield* share.unshare(info.id)
expect((yield* session.get(info.id)).share).toBeUndefined()
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/share`))).toBe(true)
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/unshare`))).toBe(true)
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.ignore)
request.mockRestore()
}),
),
Effect.provide(layer),
)
})