Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/cloud-fork-session-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Fix cloud session fork commands so they import cloud sessions before validating the local session.
26 changes: 13 additions & 13 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,6 @@ export const TuiThreadCommand = cmd({
events: createEventSource(client),
}

try {
await validateSession({
url: transport.url, // kilocode_change
sessionID: args.session,
directory: cwd,
fetch: transport.fetch,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}

setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000).unref?.()
Expand All @@ -347,6 +334,19 @@ export const TuiThreadCommand = cmd({
}
// kilocode_change end

try {
await validateSession({
url: transport.url, // kilocode_change
sessionID: args.session,
directory: cwd,
fetch: transport.fetch,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}

await start({
// kilocode_change - shared lazy loader also supports daemon attach
url: transport.url,
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/kilocode/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ export namespace KiloTuiThreadDaemon {
const prompt = await input.input()
const config = await TuiConfig.get()

const fork = await session(input, daemon)
if (!fork.ok) return true

try {
await validateSession({
url: daemon.url,
sessionID: input.args.session,
sessionID: fork.id,
directory: input.cwd,
headers: daemon.headers,
})
Expand All @@ -64,9 +67,6 @@ export namespace KiloTuiThreadDaemon {
return true
}

const fork = await session(input, daemon)
if (!fork.ok) return true

await input.start({
url: daemon.url,
config,
Expand Down
63 changes: 62 additions & 1 deletion packages/opencode/test/kilocode/cli/tui/thread.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { describe, expect, test } from "bun:test"
import { afterEach, describe, expect, mock, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { tmpdir } from "../../../fixture/fixture"
import { resolveThreadDirectory } from "../../../../src/cli/cmd/tui/thread"

afterEach(() => {
mock.restore()
})

describe("kilo tui thread", () => {
test("ignores stale PWD after cwd is changed by a process wrapper", async () => {
await using root = await tmpdir()
Expand All @@ -12,4 +16,61 @@ describe("kilo tui thread", () => {

expect(resolveThreadDirectory(".", root.path, pkg)).toBe(pkg)
})

test("imports cloud fork before validating daemon session", async () => {
const seen: string[] = []
const started: string[] = []

mock.module("@kilocode/sdk/v2", () => ({
createKiloClient: () => ({
kilo: {
cloud: {
session: {
import: async (input: { sessionId: string }) => {
expect(input.sessionId).toBe("ses_cloud")
return { data: { id: "ses_local" } }
},
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: expect inside mock will be silently swallowed

The expect(input.sessionId).toBe("ses_cloud") assertion sits inside the mocked import function. When importCloudSession is called, it invokes this mock — but the call site wraps the whole thing with .catch(() => undefined) (see thread.ts line 38). If this assertion throws (e.g. the wrong session ID is passed), the error is caught and id becomes undefined, causing an early return with fork.ok = false. The test then only fails on the later expect(seen).toEqual(["ses_local"]) with a confusing message rather than pointing directly at the bad session ID.

Consider moving the assertion out of the mock and instead capturing the received sessionId in a variable, then asserting after the await:

let importedId: string | undefined
mock.module("@kilocode/sdk/v2", () => ({
  createKiloClient: () => ({
    kilo: {
      cloud: {
        session: {
          import: async (input: { sessionId: string }) => {
            importedId = input.sessionId
            return { data: { id: "ses_local" } }
          },
        },
      },
    },
  }),
}))
// ... after `await mod.KiloTuiThreadDaemon.attach(...)`
expect(importedId).toBe("ses_cloud")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

},
}),
}))
mock.module("@/cli/cmd/tui/validate-session", () => ({
validateSession: async (input: { sessionID?: string }) => {
seen.push(input.sessionID ?? "")
},
}))
mock.module("@/cli/cmd/tui/config/tui", () => ({
TuiConfig: {
get: async () => ({}),
},
}))
mock.module("@/kilocode/daemon/client", () => ({
DaemonClient: {
maybe: async () => ({ url: "http://127.0.0.1:4096", headers: {} }),
},
}))
mock.module("@/cli/ui", () => ({
UI: {
println: () => {},
error: () => {},
},
}))

const key = JSON.stringify({ time: Date.now(), rand: Math.random() })
const mod = await import(`../../../../src/kilocode/cli/cmd/tui/thread?${key}`)

const handled = await mod.KiloTuiThreadDaemon.attach({
args: { session: "ses_cloud", cloudFork: true },
cwd: "/tmp/project",
input: async () => undefined,
start: async (input: { args: { sessionID?: string } }) => {
started.push(input.args.sessionID ?? "")
},
})

expect(handled).toBe(true)
expect(seen).toEqual(["ses_local"])
expect(started).toEqual(["ses_local"])
})
})
Loading