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
5 changes: 5 additions & 0 deletions .changeset/fix-session-scope-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Show current-worktree sessions by default in the TUI sessions dialog and keep all/current scope toggling working when a scope has no sessions.
5 changes: 5 additions & 0 deletions .changeset/list-agent-manager-worktrees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Show Agent Manager and other Git worktrees in the Kilo Console project view.
5 changes: 5 additions & 0 deletions .changeset/preserve-console-review-expansion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Keep expanded Kilo Console file diffs open while resizing the context sidebar.
5 changes: 5 additions & 0 deletions .changeset/preserve-console-terminals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Keep Kilo Console terminal sessions open when changing diff layout and other console preferences.
7 changes: 5 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@
};

kilo-dev = pkgs.writeShellScriptBin "kilo-dev" ''
cd "$KILO_ROOT"
exec ${bun}/bin/bun dev "$@"
set -euo pipefail

: "''${KILO_ROOT:?KILO_ROOT is not set. Enter the flake dev shell from the repo root.}"
export KILO_DEV_CWD="$PWD"
exec ${bun}/bin/bun --cwd "$KILO_ROOT/packages/opencode" --conditions=browser ./src/index.ts "$@"
'';

kilo-install-bin = pkgs.writeShellScriptBin "kilo-install" ''
Expand Down
5 changes: 3 additions & 2 deletions packages/kilo-console/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
VcsInfo,
Worktree,
WorktreeDiffItem,
WorktreeListResponse,
} from "@kilocode/sdk/v2/client"

export type Scope = "global" | "project"
Expand All @@ -54,7 +55,7 @@ export type ProjectConsoleSnapshot = {
project: ProjectItem
config: EffectiveConfig
vcs: VcsInfo
worktrees: string[]
worktrees: WorktreeListResponse
terminals: ProjectTerminalItem[]
}

Expand Down Expand Up @@ -447,7 +448,7 @@ export async function loadProjectConsole(input: ProjectConsoleQuery): Promise<Pr
])
const dirs = demand("Worktrees", worktrees)
const terminals = await Promise.all(
[query.dir, ...dirs].map((dir) => loadProjectTerminals({ url: input.url, dir }, dir)),
[query.dir, ...dirs.map((item) => item.directory)].map((dir) => loadProjectTerminals({ url: input.url, dir }, dir)),
)

return {
Expand Down
55 changes: 36 additions & 19 deletions packages/kilo-console/src/routes/projects/ProjectConsoleRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Context = {
dir: string
label: string
kind: "local" | "worktree"
managed: boolean
}

type Editor = { kind: "create"; value: string } | { kind: "rename"; item: Context; value: string }
Expand Down Expand Up @@ -135,6 +136,7 @@ function refreshEvent(event: ProjectConsoleEvent) {
if (type.startsWith("permission.")) return true
if (type.startsWith("question.")) return true
if (type.startsWith("message.")) return true
if (type === "global.config.updated") return true
return false
}

Expand Down Expand Up @@ -179,8 +181,14 @@ export function ProjectConsoleRoute() {
const data = snap()
if (!data) return []
return [
{ id: "local", dir: data.project.worktree, label: "Local", kind: "local" },
...data.worktrees.map((dir) => ({ id: dir, dir, label: title(dir), kind: "worktree" as const })),
{ id: "local", dir: data.project.worktree, label: "Local", kind: "local", managed: false },
...data.worktrees.map((item) => ({
id: item.directory,
dir: item.directory,
label: title(item.directory),
kind: "worktree" as const,
managed: item.managed,
})),
]
})
const terminals = createMemo(() => {
Expand Down Expand Up @@ -499,8 +507,12 @@ export function ProjectConsoleRoute() {
setEditor({ kind: "rename", item, value: displayLabel(item) })
}

function canManage(item: Context | undefined) {
return item?.kind === "worktree" && item.managed
}

function removeWorktree(item: Context) {
if (!projectInput() || item.kind === "local") return
if (!projectInput() || !canManage(item)) return
setPending({ kind: "delete", item })
}

Expand All @@ -512,7 +524,7 @@ export function ProjectConsoleRoute() {

function resetSelected() {
const item = current()
if (!projectInput() || !item || item.kind === "local") return
if (!projectInput() || !canManage(item)) return
setPending({ kind: "reset", item })
}

Expand Down Expand Up @@ -653,11 +665,14 @@ export function ProjectConsoleRoute() {
const base = query()
const data = snap()
if (!base || !data) return
const dirs = new Set([data.project.worktree, ...data.worktrees])
const dirs = new Set([data.project.worktree, ...data.worktrees.map((item) => item.directory)])
const stop = subscribeProjectEvents({ url: base.url, dir: data.project.worktree }, (event) => {
if (event.directory !== "global" && !dirs.has(event.directory)) return
const id = eventSession(event)
if (id && messageEvent(event)) markUnread(id)
// Terminal fitting emits pty.updated for every width change. Ignore those refreshes while
// dragging so the controlled review accordion keeps its expanded files mounted.
if (resize.pending && eventType(event) === "pty.updated") return
if (refreshEvent(event)) scheduleRefetch()
})
onCleanup(stop)
Expand Down Expand Up @@ -765,19 +780,21 @@ export function ProjectConsoleRoute() {
>
<Icon name="edit" size="small" />
</button>
<button
type="button"
class="project-inline-action danger"
onClick={(event) => {
event.stopPropagation()
removeWorktree(item)
}}
disabled={!!saving()}
title={`Delete ${displayLabel(item)}`}
aria-label={`Delete ${displayLabel(item)}`}
>
<Icon name="trash" size="small" />
</button>
<Show when={item.managed}>
<button
type="button"
class="project-inline-action danger"
onClick={(event) => {
event.stopPropagation()
removeWorktree(item)
}}
disabled={!!saving()}
title={`Delete ${displayLabel(item)}`}
aria-label={`Delete ${displayLabel(item)}`}
>
<Icon name="trash" size="small" />
</button>
</Show>
</Show>
</div>
</div>
Expand Down Expand Up @@ -913,7 +930,7 @@ export function ProjectConsoleRoute() {
<code class="project-info-path" title={current()?.dir}>
{current()?.dir ?? snap()?.project.worktree ?? project()}
</code>
<Show when={current()?.kind === "worktree"}>
<Show when={canManage(current())}>
<div class="project-info-actions">
<Button variant="secondary" size="small" onClick={resetSelected} disabled={!!saving()}>
Reset
Expand Down
29 changes: 10 additions & 19 deletions packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
import { Spinner } from "./spinner"
import path from "path" // kilocode_change
import { errorMessage } from "@/util/error"
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
import { WorkspaceLabel } from "./workspace-label"
Expand All @@ -31,23 +30,25 @@ export function DialogSessionList() {
const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
const [global, setGlobal] = createSignal(true) // kilocode_change - show all worktrees by default
const [global, setGlobal] = createSignal(false) // kilocode_change - show current worktree by default
const deleteHint = useCommandShortcut("session.delete")
const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")

// kilocode_change start - always fetch from experimental endpoint (returns GlobalSession with worktree info)
// TODO: extend /experimental/session to accept `scope`/`path` so this dialog can respect the
// upstream `session_directory_filter_enabled` KV toggle (via sync.session.query()) while
// keeping worktree grouping. Currently the toggle has no effect here.
// keeping worktree grouping.
const [searchResults, searchActions] = createResource(
() => search(),
async (query) => {
() => ({ query: search(), global: global(), directory: project.instance.directory() }), // kilocode_change
async (input) => {
const result = await sdk.client.experimental.session.list(
{
search: query || undefined,
search: input.query || undefined,
roots: true,
worktrees: true,
current: input.global ? undefined : "true",
directory: input.global ? undefined : input.directory || undefined,
limit: 30,
},
{ throwOnError: true },
Expand All @@ -59,15 +60,7 @@ export function DialogSessionList() {

const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))

// kilocode_change start - client-side worktree filtering when global is off
const sessions = createMemo(() => {
const all = searchResults() ?? []
if (global()) return all
const root = project.instance.path().worktree
if (!root || root === "/") return all
return all.filter((s) => s.directory === root || s.directory.startsWith(root + path.sep))
})
// kilocode_change end
const sessions = createMemo(() => searchResults() ?? []) // kilocode_change - endpoint applies worktree scope

function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
const workspace = project.workspace.get(session.workspaceID!)
Expand Down Expand Up @@ -154,8 +147,6 @@ export function DialogSessionList() {
.map((x) => x.id)
}

const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))

const quickSwitchHint = createMemo(() => {
const first = quickSwitch1()
const last = quickSwitch9()
Expand All @@ -176,8 +167,7 @@ export function DialogSessionList() {
.map((x) => [x.id, x]),
)

const searchResult = searchResults()
const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
const displayOrder = orderByRecency(sessions()) // kilocode_change - respect current scope

const pinned = local.session.pinned().filter((id) => sessionMap.has(id))
const pinnedSet = new Set(pinned)
Expand Down Expand Up @@ -335,6 +325,7 @@ export function DialogSessionList() {
{
command: "session.scope.toggle",
title: global() ? "current" : "all",
requiresSelection: false,
onTrigger: async () => {
setToDelete(undefined)
setGlobal((v) => !v)
Expand Down
11 changes: 8 additions & 3 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,17 @@ async function input(value?: string) {
}

export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
// kilocode_change start - ignore stale PWD from wrappers such as `bun --cwd`
// kilocode_change start - ignore stale PWD from wrappers such as `bun --cwd`, except kilo-dev's caller cwd
const dev = process.env.KILO_DEV_CWD
const real = Filesystem.resolve(cwd)
const root = envPWD && Filesystem.resolve(envPWD) === real ? Filesystem.resolve(envPWD) : real
const root = dev
? Filesystem.resolve(dev)
: envPWD && Filesystem.resolve(envPWD) === real
? Filesystem.resolve(envPWD)
: real
// kilocode_change end
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
return real // kilocode_change
return dev ? root : real // kilocode_change
}

export const TuiThreadCommand = cmd({
Expand Down
35 changes: 28 additions & 7 deletions packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,7 @@ export interface DialogSelectProps<T> {
onSelect?: (option: DialogSelectOption<T>) => void
skipFilter?: boolean
renderFilter?: boolean
actions?: {
command: string
title: string
side?: "left" | "right"
disabled?: boolean
onTrigger: (option: DialogSelectOption<T>) => void
}[]
actions?: DialogSelectAction<T>[] // kilocode_change - supports actions without a selected option
footerHints?: {
title: string
label: string
Expand All @@ -47,6 +41,27 @@ export interface DialogSelectProps<T> {
current?: T
}

// kilocode_change start - support list-level actions when no option is selected
type DialogSelectActionBase = {
command: string
title: string
side?: "left" | "right"
disabled?: boolean
}

type DialogSelectAction<T> = DialogSelectActionBase &
(
| {
requiresSelection?: true
onTrigger: (option: DialogSelectOption<T>) => void
}
| {
requiresSelection: false
onTrigger: () => void
}
)
// kilocode_change end

export interface DialogSelectOption<T = any> {
title: string
value: T
Expand Down Expand Up @@ -303,6 +318,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
category: "Dialog",
run() {
setStore("input", "keyboard")
// kilocode_change start - allow actions such as scope toggles on empty lists
if (item.requiresSelection === false) {
item.onTrigger()
return
}
// kilocode_change end
const option = selected()
if (!option) return
item.onTrigger(option)
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/effect/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ function captureSync() {
export const bind = <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => {
const captured = captureSync()
return (...args: Args) =>
restore(captured.instance, captured.workspace, () => // kilocode_change
restore(captured.instance, captured.workspace, () =>
// kilocode_change
Effect.runSync(
attachWith(
Effect.sync(() => fn(...args)),
Expand Down
16 changes: 14 additions & 2 deletions packages/opencode/src/installation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import semver from "semver"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { NpmConfig } from "@opencode-ai/core/npm-config"
// kilocode_change start
import { Brew as KiloBrew, Choco as KiloChoco, Npm as KiloNpm, Release as KiloRelease, Scoop as KiloScoop } from "@/kilocode/installation"
import {
Brew as KiloBrew,
Choco as KiloChoco,
Npm as KiloNpm,
Release as KiloRelease,
Scoop as KiloScoop,
} from "@/kilocode/installation"
// kilocode_change end

const log = Log.create({ service: "installation" })
Expand Down Expand Up @@ -224,7 +230,13 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
return data.versions.stable
}

if (detectedMethod === "npm" || detectedMethod === "yarn" || detectedMethod === "bun" || detectedMethod === "pnpm") { // kilocode_change
if (
detectedMethod === "npm" ||
detectedMethod === "yarn" ||
detectedMethod === "bun" ||
detectedMethod === "pnpm"
) {
// kilocode_change
const response = yield* httpOk.execute(
HttpClientRequest.get(
`${yield* NpmConfig.registry(process.cwd())}/${KiloNpm.path}/${InstallationChannel}`, // kilocode_change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ export const configConsoleHandlers = HttpApiBuilder.group(InstanceHttpApi, "conf
return yield* config.get()
}
if (body.scope === "global") {
const result = yield* config.updateGlobal(patch)
if (result.changed) {
const hot = Object.keys(patch).every((key) => key === "console")
const result = yield* config.updateGlobal(patch, hot ? { dispose: false } : undefined)
if (result.changed && !hot) {
yield* disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(
Effect.catchCause(() => Effect.void),
)
Expand Down
Loading
Loading