resolve merge conflicts
This commit is contained in:
@@ -244,10 +244,7 @@ export const ExportCommand = cmd({
|
||||
output: process.stderr,
|
||||
})
|
||||
|
||||
const sessions = []
|
||||
for await (const session of Session.list()) {
|
||||
sessions.push(session)
|
||||
}
|
||||
const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list()))
|
||||
|
||||
if (sessions.length === 0) {
|
||||
prompts.log.error("No sessions found", {
|
||||
|
||||
@@ -212,7 +212,7 @@ export const GithubInstallCommand = cmd({
|
||||
const app = await getAppInfo()
|
||||
await installGitHubApp()
|
||||
|
||||
const providers = await ModelsDev.get().then((p) => {
|
||||
const providers = await AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get())).then((p) => {
|
||||
// TODO: add guide for copilot, for now just hide it
|
||||
delete p["github-copilot"]
|
||||
return p
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { cmd } from "./cmd"
|
||||
import { effectCmd, fail } from "../effect-cmd"
|
||||
import { UI } from "../ui"
|
||||
import { EOL } from "os"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const ModelsCommand = cmd({
|
||||
export const ModelsCommand = effectCmd({
|
||||
command: "models [provider]",
|
||||
describe: "list all available models",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("provider", {
|
||||
describe: "provider ID to filter models by",
|
||||
type: "string",
|
||||
@@ -26,65 +24,46 @@ export const ModelsCommand = cmd({
|
||||
.option("refresh", {
|
||||
describe: "refresh the models cache from models.dev",
|
||||
type: "boolean",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
}),
|
||||
handler: Effect.fn("Cli.models")(function* (args) {
|
||||
if (args.refresh) {
|
||||
await ModelsDev.refresh(true)
|
||||
yield* ModelsDev.Service.use((s) => s.refresh(true))
|
||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
||||
}
|
||||
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Provider.Service
|
||||
const providers = yield* svc.list()
|
||||
const provider = yield* Provider.Service
|
||||
const providers = yield* provider.list()
|
||||
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const provider = providers[providerID]
|
||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const p = providers[providerID]
|
||||
const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
const provider = providers[providerID]
|
||||
if (!provider) {
|
||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`)
|
||||
print(providerID, args.verbose)
|
||||
return
|
||||
}
|
||||
|
||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
||||
return
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsKilo = a === "kilo" || a.startsWith("opencode")
|
||||
const bIsKilo = b === "kilo" || b.startsWith("opencode")
|
||||
if (aIsKilo && !bIsKilo) return -1
|
||||
if (!aIsKilo && bIsKilo) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
for (const providerID of ids) {
|
||||
print(ProviderID.make(providerID), args.verbose)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
// kilocode_change start
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsKilo = a === "kilo" || a.startsWith("opencode")
|
||||
const bIsKilo = b === "kilo" || b.startsWith("opencode")
|
||||
if (aIsKilo && !bIsKilo) return -1
|
||||
if (!aIsKilo && bIsKilo) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
},
|
||||
// kilocode_change end
|
||||
|
||||
for (const providerID of ids) print(ProviderID.make(providerID), args.verbose)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,9 @@ import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
|
||||
const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
|
||||
const refreshModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.refresh(true)))
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
@@ -156,28 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
||||
}
|
||||
|
||||
if (method.type === "api") {
|
||||
if (method.authorize) {
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
|
||||
const result = await method.authorize(inputs)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
const saveProvider = result.provider ?? provider
|
||||
await put(saveProvider, {
|
||||
type: "api",
|
||||
key: result.key ?? key,
|
||||
})
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
const metadata = Object.keys(inputs).length ? { metadata: inputs } : {}
|
||||
if (!method.authorize) {
|
||||
await put(provider, {
|
||||
type: "api",
|
||||
key,
|
||||
...metadata,
|
||||
})
|
||||
prompts.outro("Done")
|
||||
return true
|
||||
}
|
||||
|
||||
const result = await method.authorize(inputs)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
const saveProvider = result.provider ?? provider
|
||||
await put(saveProvider, {
|
||||
type: "api",
|
||||
key: result.key ?? key,
|
||||
...metadata,
|
||||
})
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
prompts.outro("Done")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -241,7 +254,7 @@ export const ProvidersListCommand = cmd({
|
||||
return Object.entries(yield* auth.all())
|
||||
}),
|
||||
)
|
||||
const database = await ModelsDev.get()
|
||||
const database = await getModels()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
const name = database[providerID]?.name || providerID
|
||||
@@ -334,14 +347,14 @@ export const ProvidersLoginCommand = cmd({
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
await ModelsDev.refresh(true).catch(() => {})
|
||||
await refreshModels().catch(() => {})
|
||||
|
||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
|
||||
const providers = await ModelsDev.get().then((x) => {
|
||||
const providers = await getModels().then((x) => {
|
||||
const filtered: Record<string, (typeof x)[string]> = {}
|
||||
for (const [key, value] of Object.entries(x)) {
|
||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||
@@ -512,7 +525,7 @@ export const ProvidersLogoutCommand = cmd({
|
||||
prompts.log.error("No credentials found")
|
||||
return
|
||||
}
|
||||
const database = await ModelsDev.get()
|
||||
const database = await getModels()
|
||||
const selected = await prompts.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { KiloSessions } from "@/kilo-sessions/kilo-sessions"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
|
||||
export const RemoteCommand = cmd({
|
||||
command: "remote",
|
||||
@@ -17,7 +18,7 @@ export const RemoteCommand = cmd({
|
||||
const shutdown = async () => {
|
||||
try {
|
||||
KiloSessions.disableRemote()
|
||||
await Instance.dispose()
|
||||
await InstanceStore.disposeInstance(Instance.current)
|
||||
} finally {
|
||||
abort.abort()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Server } from "../../server/server"
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../project/instance" // kilocode_change
|
||||
import { InstanceStore } from "../../project/instance-store" // kilocode_change
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
command: "serve",
|
||||
@@ -20,7 +20,7 @@ export const ServeCommand = cmd({
|
||||
const abort = new AbortController()
|
||||
const shutdown = async () => {
|
||||
try {
|
||||
await Instance.disposeAll()
|
||||
await InstanceStore.disposeAllInstances()
|
||||
await server.stop(true)
|
||||
} finally {
|
||||
abort.abort()
|
||||
|
||||
@@ -113,7 +113,9 @@ export const SessionListCommand = cmd({
|
||||
// kilocode_change start
|
||||
const sessions = args.all
|
||||
? [...Session.listGlobal({ roots: true, limit: args.maxCount, search: args.search })]
|
||||
: [...Session.list({ roots: true, limit: args.maxCount, search: args.search })]
|
||||
: await AppRuntime.runPromise(
|
||||
Session.Service.use((svc) => svc.list({ roots: true, limit: args.maxCount, search: args.search })),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start
|
||||
|
||||
@@ -140,6 +140,8 @@ export function tui(input: {
|
||||
process.on("exit", resetTerminalState) // kilocode_change
|
||||
|
||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||
// Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
|
||||
void renderer.getPalette({ size: 16 }).catch(() => undefined)
|
||||
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||
|
||||
await render(() => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export function createDialogProviderOptions() {
|
||||
description: KiloProvider.PROVIDER_DESCRIPTIONS[provider.id], // kilocode_change
|
||||
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
gutter: connected && onboarded() ? <text fg={theme.success}>✓</text> : undefined,
|
||||
gutter: connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined,
|
||||
async onSelect() {
|
||||
if (consoleManaged) return
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ export function DialogSessionList() {
|
||||
value: x.id,
|
||||
category,
|
||||
footer,
|
||||
gutter: isWorking ? <Spinner /> : undefined,
|
||||
gutter: isWorking ? () => <Spinner /> : undefined,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
type Adaptor = {
|
||||
type Adapter = {
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
@@ -108,26 +108,26 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const [creating, setCreating] = createSignal<string>()
|
||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
||||
const [adapters, setAdapters] = createSignal<Adapter[]>()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
void (async () => {
|
||||
const dir = sync.path.directory || sdk.directory
|
||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
||||
const url = new URL("/experimental/workspace/adapter", sdk.url)
|
||||
if (dir) url.searchParams.set("directory", dir)
|
||||
const res = await sdk
|
||||
.fetch(url)
|
||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
||||
.then((x) => x.json() as Promise<Adapter[]>)
|
||||
.catch(() => undefined)
|
||||
if (!res) {
|
||||
toast.show({
|
||||
message: "Failed to load workspace adaptors",
|
||||
message: "Failed to load workspace adapters",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
setAdaptors(res)
|
||||
setAdapters(res)
|
||||
})()
|
||||
})
|
||||
|
||||
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
||||
},
|
||||
]
|
||||
}
|
||||
const list = adaptors()
|
||||
const list = adapters()
|
||||
if (!list) {
|
||||
return [
|
||||
{
|
||||
title: "Loading workspaces...",
|
||||
value: "loading" as const,
|
||||
description: "Fetching available workspace adaptors",
|
||||
description: "Fetching available workspace adapters",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
|
||||
import { useTheme, tint } from "@tui/context/theme"
|
||||
import { KiloLogo } from "./kilo-logo" // kilocode_change
|
||||
|
||||
@@ -12,11 +12,12 @@ import { useRoute } from "@tui/context/route"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { useEditorContext, type EditorSelection } from "@tui/context/editor"
|
||||
import { editorSelectionKey, useEditorContext, type EditorSelection } from "@tui/context/editor"
|
||||
import { MessageID, PartID } from "@/session/schema"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
import { computePromptTraits } from "./traits"
|
||||
import { assign } from "./part"
|
||||
import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
@@ -84,16 +85,30 @@ function fadeColor(color: RGBA, alpha: number) {
|
||||
return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha)
|
||||
}
|
||||
|
||||
function getEditorSelectionKey(selection: EditorSelection) {
|
||||
return [
|
||||
selection.filePath,
|
||||
selection.text,
|
||||
selection.source ?? "",
|
||||
selection.selection.start.line,
|
||||
selection.selection.start.character,
|
||||
selection.selection.end.line,
|
||||
selection.selection.end.character,
|
||||
].join("-")
|
||||
function hasEditorRangeSelection(selection: EditorSelection["ranges"][number]) {
|
||||
return (
|
||||
selection.selection.start.line !== selection.selection.end.line ||
|
||||
selection.selection.start.character !== selection.selection.end.character
|
||||
)
|
||||
}
|
||||
|
||||
function getEditorRangeLabel(selection: EditorSelection["ranges"][number]) {
|
||||
if (!hasEditorRangeSelection(selection)) return
|
||||
if (selection.selection.start.line === selection.selection.end.line) return `#${selection.selection.start.line}`
|
||||
return `#${selection.selection.start.line}-${selection.selection.end.line}`
|
||||
}
|
||||
|
||||
function formatEditorContext(selection: EditorSelection) {
|
||||
const selected = selection.ranges.filter(hasEditorRangeSelection)
|
||||
if (selected.length === 0)
|
||||
return `<system-reminder>Note: The user opened the file "${selection.filePath}". This may or may not be relevant to the current task.</system-reminder>\n`
|
||||
|
||||
const ranges = selected.map((range, index) => {
|
||||
const prefix = selected.length > 1 ? `Selection ${index + 1}: ` : ""
|
||||
return `Note: The user selected ${prefix}${getEditorRangeLabel(range)} from "${selection.filePath}". \`\`\`${range.text}\`\`\`\n\n`
|
||||
})
|
||||
|
||||
return `<system-reminder>${ranges.join("\n")} This may or may not be relevant to the current task.</system-reminder>\n`
|
||||
}
|
||||
|
||||
let stashed: { prompt: PromptInfo; cursor: number } | undefined
|
||||
@@ -125,13 +140,21 @@ export function Prompt(props: PromptProps) {
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const fileContextEnabled = createMemo(() => kv.get("file_context_enabled", true))
|
||||
const editorPath = createMemo(() => (fileContextEnabled() ? editor.selection()?.filePath : undefined))
|
||||
const editorSelectionLabel = createMemo(() => {
|
||||
const selection = fileContextEnabled() ? editor.selection()?.selection : undefined
|
||||
const [dismissedEditorSelectionKey, setDismissedEditorSelectionKey] = createSignal<string>()
|
||||
const editorContext = createMemo(() => {
|
||||
const selection = fileContextEnabled() ? editor.selection() : undefined
|
||||
if (!selection) return
|
||||
if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return
|
||||
if (selection.start.line === selection.end.line) return `#${selection.start.line}`
|
||||
return `#${selection.start.line}-${selection.end.line}`
|
||||
return editorSelectionKey(selection) === dismissedEditorSelectionKey() ? undefined : selection
|
||||
})
|
||||
const editorPath = createMemo(() => editorContext()?.filePath)
|
||||
const editorSelectionLabel = createMemo(() => {
|
||||
const ranges = editorContext()?.ranges
|
||||
if (!ranges) return
|
||||
const first = ranges.find(hasEditorRangeSelection) ?? ranges[0]
|
||||
if (!first) return
|
||||
return [getEditorRangeLabel(first), ranges.length > 1 ? `+${ranges.length - 1}` : undefined]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
})
|
||||
const editorFileLabel = createMemo(() => {
|
||||
const value = editorPath()
|
||||
@@ -147,6 +170,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (!file) return
|
||||
return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
||||
})
|
||||
const [editorContextHover, setEditorContextHover] = createSignal(false)
|
||||
let lastSubmittedEditorSelectionKey: string | undefined
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
@@ -163,6 +187,11 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function dismissEditorContext() {
|
||||
setDismissedEditorSelectionKey(editorSelectionKey(editorContext()))
|
||||
editor.clearSelection()
|
||||
}
|
||||
|
||||
const textareaKeybindings = useTextareaKeybindings()
|
||||
|
||||
const fileStyleId = syntax().getStyleId("extmark.file")!
|
||||
@@ -294,6 +323,16 @@ export function Prompt(props: PromptProps) {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Remove editor context",
|
||||
value: "prompt.editor_context.clear",
|
||||
category: "Prompt",
|
||||
enabled: Boolean(editorContext()),
|
||||
onSelect: (dialog) => {
|
||||
dismissEditorContext()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Paste",
|
||||
value: "prompt.paste",
|
||||
@@ -529,17 +568,11 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
const capture =
|
||||
store.mode === "normal"
|
||||
? auto()?.visible
|
||||
? (["escape", "navigate", "submit", "tab"] as const)
|
||||
: (["tab"] as const)
|
||||
: undefined
|
||||
input.traits = {
|
||||
capture,
|
||||
suspend: !!props.disabled || store.mode === "shell",
|
||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
input.traits = computePromptTraits({
|
||||
mode: store.mode,
|
||||
disabled: !!props.disabled,
|
||||
autocompleteVisible: !!auto()?.visible,
|
||||
})
|
||||
})
|
||||
|
||||
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
||||
@@ -770,35 +803,21 @@ export function Prompt(props: PromptProps) {
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
const editorSelection = fileContextEnabled() ? editor.selection() : undefined
|
||||
const editorSelectionKey = editorSelection ? getEditorSelectionKey(editorSelection) : undefined
|
||||
const editorSelection = editorContext()
|
||||
const currentEditorSelectionKey = editorSelectionKey(editorSelection)
|
||||
const editorParts =
|
||||
editorSelection && editorSelectionKey !== lastSubmittedEditorSelectionKey
|
||||
editorSelection && currentEditorSelectionKey !== lastSubmittedEditorSelectionKey
|
||||
? [
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
type: "text" as const,
|
||||
text: (() => {
|
||||
const start = editorSelection.selection.start
|
||||
const end = editorSelection.selection.end
|
||||
|
||||
let text = ""
|
||||
if (start.line === end.line && start.character === end.character) {
|
||||
text = `Note: The user opened the file "${editorSelection.filePath}".`
|
||||
} else if (start.line === end.line) {
|
||||
text = `Note: The user selected line ${start.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
|
||||
} else {
|
||||
text = `Note: The user selected lines ${start.line + 1} to ${end.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
|
||||
}
|
||||
|
||||
return `<system-reminder>${text} This may or may not be relevant to the current task.</system-reminder>\n`
|
||||
})(),
|
||||
text: formatEditorContext(editorSelection),
|
||||
synthetic: true,
|
||||
metadata: {
|
||||
kind: "editor_context",
|
||||
source: editorSelection.source ?? "editor",
|
||||
filePath: editorSelection.filePath,
|
||||
selection: editorSelection.selection,
|
||||
ranges: editorSelection.ranges,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -865,7 +884,7 @@ export function Prompt(props: PromptProps) {
|
||||
],
|
||||
})
|
||||
.catch(() => {})
|
||||
lastSubmittedEditorSelectionKey = editorSelectionKey
|
||||
lastSubmittedEditorSelectionKey = currentEditorSelectionKey
|
||||
}
|
||||
toast.dismiss() // kilocode_change - dismiss persistent config warning on first submit
|
||||
history.append({
|
||||
@@ -1450,7 +1469,18 @@ export function Prompt(props: PromptProps) {
|
||||
</text>
|
||||
</Show>
|
||||
{/* kilocode_change end */}
|
||||
<Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
|
||||
<Show when={editorFileLabelDisplay()}>
|
||||
{(file) => (
|
||||
<text
|
||||
fg={theme.secondary}
|
||||
onMouseOver={() => setEditorContextHover(true)}
|
||||
onMouseOut={() => setEditorContextHover(false)}
|
||||
onMouseUp={dismissEditorContext}
|
||||
>
|
||||
{editorContextHover() ? `x ${file()}` : file()}
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Switch>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { EditorTraits } from "@opentui/core"
|
||||
|
||||
export type PromptMode = "normal" | "shell"
|
||||
|
||||
export interface PromptTraitsInput {
|
||||
mode: PromptMode
|
||||
disabled: boolean
|
||||
autocompleteVisible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the textarea editor traits for the prompt.
|
||||
*
|
||||
* `traits.suspend` gates the textarea's keybinding actions (backspace,
|
||||
* delete-word, arrow movement, undo/redo, etc.). Shell mode is an active
|
||||
* editing mode — only `disabled` should suspend the textarea, otherwise
|
||||
* users can type in shell mode but cannot delete or move the cursor.
|
||||
*/
|
||||
export function computePromptTraits(input: PromptTraitsInput): EditorTraits {
|
||||
const capture =
|
||||
input.mode === "normal"
|
||||
? input.autocompleteVisible
|
||||
? (["escape", "navigate", "submit", "tab"] as const)
|
||||
: (["tab"] as const)
|
||||
: undefined
|
||||
return {
|
||||
capture,
|
||||
suspend: input.disabled,
|
||||
status: input.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ const ZedEditorRowSchema = z.object({
|
||||
workspace_paths: z.string().nullable(),
|
||||
timestamp: z.string(),
|
||||
buffer_path: z.string().nullable(),
|
||||
})
|
||||
|
||||
const ZedSelectionRowSchema = z.object({
|
||||
selection_start: z.number().nullable(),
|
||||
selection_end: z.number().nullable(),
|
||||
})
|
||||
@@ -24,6 +27,7 @@ const utf8 = new TextEncoder()
|
||||
|
||||
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
|
||||
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
||||
type ZedSelectionRow = z.infer<typeof ZedSelectionRowSchema>
|
||||
|
||||
export type ZedSelectionResult =
|
||||
| { type: "selection"; selection: EditorSelection }
|
||||
@@ -36,7 +40,21 @@ export async function resolveZedSelection(dbPath: string, cwd = process.cwd()):
|
||||
|
||||
const row = active.row
|
||||
if (!row.buffer_path) return { type: "empty" }
|
||||
if (row.selection_start == null || row.selection_end == null) return { type: "unavailable" }
|
||||
|
||||
const selections = queryZedEditorSelections(dbPath, row)
|
||||
if (selections.type !== "selections") return selections
|
||||
const byteRanges = selections.selections
|
||||
.flatMap((selection) => {
|
||||
if (selection.selection_start == null || selection.selection_end == null) return []
|
||||
return [
|
||||
{
|
||||
start: Math.min(selection.selection_start, selection.selection_end),
|
||||
end: Math.max(selection.selection_start, selection.selection_end),
|
||||
},
|
||||
]
|
||||
})
|
||||
.sort((left, right) => left.start - right.start || left.end - right.end)
|
||||
if (byteRanges.length === 0) return { type: "unavailable" }
|
||||
|
||||
const contents = queryZedEditorContents(dbPath, row)
|
||||
const text =
|
||||
@@ -47,16 +65,21 @@ export async function resolveZedSelection(dbPath: string, cwd = process.cwd()):
|
||||
.catch(() => undefined)
|
||||
if (text == null) return { type: "unavailable" }
|
||||
|
||||
const startOffset = utf8ByteOffsetToStringIndex(text, Math.min(row.selection_start, row.selection_end))
|
||||
const endOffset = utf8ByteOffsetToStringIndex(text, Math.max(row.selection_start, row.selection_end))
|
||||
const ranges = byteRanges.map((range) => {
|
||||
const startOffset = utf8ByteOffsetToStringIndex(text, range.start)
|
||||
const endOffset = utf8ByteOffsetToStringIndex(text, range.end)
|
||||
return {
|
||||
text: text.slice(startOffset, endOffset),
|
||||
selection: offsetsToSelection(text, startOffset, endOffset),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
type: "selection",
|
||||
selection: {
|
||||
text: text.slice(startOffset, endOffset),
|
||||
filePath: row.buffer_path,
|
||||
source: "zed",
|
||||
selection: offsetsToSelection(text, startOffset, endOffset),
|
||||
ranges,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -73,14 +96,11 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
|
||||
i.workspace_id as workspace_id,
|
||||
w.paths as workspace_paths,
|
||||
w.timestamp as timestamp,
|
||||
e.buffer_path as buffer_path,
|
||||
s.start as selection_start,
|
||||
s.end as selection_end
|
||||
e.buffer_path as buffer_path
|
||||
from items i
|
||||
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
|
||||
join workspaces w on w.workspace_id = i.workspace_id
|
||||
left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
|
||||
left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id
|
||||
where i.active = 1 and p.active = 1
|
||||
order by w.timestamp desc`,
|
||||
)
|
||||
@@ -108,6 +128,34 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
|
||||
let db: Database | undefined
|
||||
try {
|
||||
db = new Database(dbPath, { readonly: true })
|
||||
const raw = db
|
||||
.query(
|
||||
`select
|
||||
start as selection_start,
|
||||
end as selection_end
|
||||
from editor_selections
|
||||
where editor_id = $editorID and workspace_id = $workspaceID`,
|
||||
)
|
||||
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
||||
|
||||
const selections = raw.flatMap((selection) => {
|
||||
const parsed = ZedSelectionRowSchema.safeParse(selection)
|
||||
return parsed.success ? [parsed.data] : []
|
||||
})
|
||||
|
||||
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
||||
return { type: "selections" as const, selections }
|
||||
} catch {
|
||||
return { type: "unavailable" as const }
|
||||
} finally {
|
||||
db?.close()
|
||||
}
|
||||
}
|
||||
|
||||
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
||||
let db: Database | undefined
|
||||
try {
|
||||
@@ -141,13 +189,20 @@ export function resolveZedDbPath() {
|
||||
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
|
||||
].filter((item): item is string => Boolean(item))
|
||||
|
||||
return candidates.find((item) => Filesystem.stat(item)?.isFile())
|
||||
return candidates.find((item) => isFile(item))
|
||||
}
|
||||
|
||||
function isFile(item: string) {
|
||||
try {
|
||||
return Filesystem.stat(item)?.isFile() === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
|
||||
return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
|
||||
if (pathContains(item, cwd)) return Math.max(score, 2)
|
||||
if (pathContains(cwd, item)) return Math.max(score, 1)
|
||||
if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
|
||||
return score
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@@ -28,16 +28,46 @@ const PositionSchema = z.object({
|
||||
character: z.number(),
|
||||
})
|
||||
|
||||
const EditorSelectionSchema = z.object({
|
||||
const EditorSelectionRangeSchema = z.object({
|
||||
text: z.string(),
|
||||
filePath: z.string(),
|
||||
source: z.enum(["websocket", "zed"]).optional(),
|
||||
selection: z.object({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
const EditorSelectionSchema = z
|
||||
.union([
|
||||
z.object({
|
||||
filePath: z.string(),
|
||||
source: z.enum(["websocket", "zed"]).optional(),
|
||||
ranges: z.array(EditorSelectionRangeSchema).min(1),
|
||||
}),
|
||||
z.object({
|
||||
text: z.string(),
|
||||
filePath: z.string(),
|
||||
source: z.enum(["websocket", "zed"]).optional(),
|
||||
selection: z.object({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
.transform((value) =>
|
||||
"ranges" in value
|
||||
? value
|
||||
: {
|
||||
filePath: value.filePath,
|
||||
source: value.source,
|
||||
ranges: [
|
||||
{
|
||||
text: value.text,
|
||||
selection: value.selection,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
const EditorMentionSchema = z.object({
|
||||
filePath: z.string(),
|
||||
lineStart: z.number(),
|
||||
@@ -262,6 +292,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||
return store.selection
|
||||
},
|
||||
clearSelection() {
|
||||
lastZedSelectionKey = undefined
|
||||
setStore("selection", undefined)
|
||||
},
|
||||
onMention(listener: (mention: EditorMention) => void) {
|
||||
@@ -352,15 +383,17 @@ function readEditorLockFile(filePath: string): EditorLockFile | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function editorSelectionKey(selection: EditorSelection | undefined) {
|
||||
export function editorSelectionKey(selection: EditorSelection | undefined) {
|
||||
if (!selection) return ""
|
||||
return [
|
||||
selection.filePath,
|
||||
selection.selection.start.line,
|
||||
selection.selection.start.character,
|
||||
selection.selection.end.line,
|
||||
selection.selection.end.character,
|
||||
selection.text,
|
||||
...selection.ranges.flatMap((range) => [
|
||||
range.selection.start.line,
|
||||
range.selection.start.character,
|
||||
range.selection.end.line,
|
||||
range.selection.end.character,
|
||||
range.text,
|
||||
]),
|
||||
].join("\0")
|
||||
}
|
||||
|
||||
|
||||
@@ -430,12 +430,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
// kilocode_change start - safe fallback to kilo import if store lookup fails
|
||||
const values = createMemo(() => {
|
||||
const active = store.themes[store.active]
|
||||
if (active) return resolveTheme(active, store.mode)
|
||||
if (active) {
|
||||
return resolveTheme(active, store.mode)
|
||||
}
|
||||
|
||||
const saved = kv.get("theme")
|
||||
if (typeof saved === "string") {
|
||||
const theme = store.themes[saved]
|
||||
if (theme) return resolveTheme(theme, store.mode)
|
||||
if (theme) {
|
||||
return resolveTheme(theme, store.mode)
|
||||
}
|
||||
}
|
||||
|
||||
return resolveTheme(store.themes.kilo, store.mode) // kilocode_change
|
||||
|
||||
@@ -74,6 +74,12 @@ async function input(value?: string) {
|
||||
return piped + "\n" + value
|
||||
}
|
||||
|
||||
export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
|
||||
const root = Filesystem.resolve(envPWD ?? cwd)
|
||||
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
|
||||
return Filesystem.resolve(cwd)
|
||||
}
|
||||
|
||||
export const TuiThreadCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start kilo tui", // kilocode_change
|
||||
@@ -143,10 +149,7 @@ export const TuiThreadCommand = cmd({
|
||||
|
||||
// Resolve relative --project paths from PWD, then use the real cwd after
|
||||
// chdir so the thread and worker share the same directory key.
|
||||
const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
|
||||
const next = args.project
|
||||
? Filesystem.resolve(path.isAbsolute(args.project) ? args.project : path.join(root, args.project))
|
||||
: Filesystem.resolve(process.cwd())
|
||||
const next = resolveThreadDirectory(args.project)
|
||||
const file = await target()
|
||||
try {
|
||||
process.chdir(next)
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface DialogSelectOption<T = any> {
|
||||
categoryView?: JSX.Element
|
||||
disabled?: boolean
|
||||
bg?: RGBA
|
||||
gutter?: JSX.Element
|
||||
gutter?: () => JSX.Element
|
||||
margin?: JSX.Element
|
||||
onSelect?: (ctx: DialogContext) => void
|
||||
}
|
||||
@@ -407,7 +407,7 @@ function Option(props: {
|
||||
active?: boolean
|
||||
current?: boolean
|
||||
footer?: JSX.Element | string
|
||||
gutter?: JSX.Element
|
||||
gutter?: () => JSX.Element
|
||||
onMouseOver?: () => void
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
@@ -422,7 +422,7 @@ function Option(props: {
|
||||
</Show>
|
||||
<Show when={!props.current && props.gutter}>
|
||||
<box flexShrink={0} marginRight={0}>
|
||||
{props.gutter}
|
||||
{props.gutter?.()}
|
||||
</box>
|
||||
</Show>
|
||||
<text
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Installation } from "@/installation"
|
||||
import { Server } from "@/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { upgrade } from "@/cli/upgrade"
|
||||
import { Config } from "@/config/config"
|
||||
@@ -10,7 +10,7 @@ import { GlobalBus } from "@/bus/global"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { writeHeapSnapshot } from "node:v8"
|
||||
import { Heap } from "@/cli/heap"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AppRuntime, getBootstrapRunEffect } from "@/effect/app-runtime"
|
||||
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
|
||||
|
||||
ensureProcessMetadata("worker")
|
||||
@@ -77,7 +77,7 @@ export const rpc = {
|
||||
async checkUpgrade(input: { directory: string }) {
|
||||
await Instance.provide({
|
||||
directory: input.directory,
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
init: await getBootstrapRunEffect(),
|
||||
fn: async () => {
|
||||
await upgrade().catch(() => {})
|
||||
},
|
||||
@@ -89,7 +89,7 @@ export const rpc = {
|
||||
async shutdown() {
|
||||
Log.Default.info("worker shutting down")
|
||||
|
||||
await Instance.disposeAll()
|
||||
await InstanceStore.disposeAllInstances()
|
||||
if (server) await server.stop(true)
|
||||
// kilocode_change start - Clear the Rpc message channel so the worker's event loop can drain and
|
||||
// exit naturally. Without this, the active onmessage handle keeps the
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../project/instance" // kilocode_change
|
||||
import { InstanceStore } from "../../project/instance-store" // kilocode_change
|
||||
import open from "open"
|
||||
import { networkInterfaces } from "os"
|
||||
|
||||
@@ -80,7 +80,7 @@ export const WebCommand = cmd({
|
||||
const abort = new AbortController()
|
||||
const shutdown = async () => {
|
||||
try {
|
||||
await Instance.disposeAll()
|
||||
await InstanceStore.disposeAllInstances()
|
||||
await server.stop(true)
|
||||
} finally {
|
||||
abort.abort()
|
||||
|
||||
Reference in New Issue
Block a user