Skip to content
Open
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/prompt-navigator-position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add a Display setting to choose whether the prompt navigator rail sits on the left or right edge of the chat. The rail, its hover card, and its open animation mirror to the selected side, and the choice applies everywhere the transcript is shown, including the sidebar, Kilo editor tabs, the sub-agent viewer, and Agent Manager. Left remains the default.
5 changes: 5 additions & 0 deletions packages/core/src/v1/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export const Info = Schema.Struct({
description:
"Controls whether MCP and generic tool blocks are expanded or collapsed by default in the VS Code chat UI",
}),
// kilocode_change start
prompt_rail_position: Schema.optional(Schema.Literals(["left", "right"])).annotate({

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.

SUGGESTION: Mirror prompt_rail_position in the cloud config JSON Schema

This file notes that new kilocode_change keys on Config.Info must also be added in apps/web/src/app/config.json/extras.ts in the cloud repo. Until that lands, $schema: https://app.kilo.ai/config.json will treat the key as unknown. CI does not check this.


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

description: "Controls which edge of the chat the prompt navigator rail appears on",
}),
// kilocode_change end
hide_prompt_training_models: Schema.optional(Schema.Boolean).annotate({
description: "Hide Kilo Gateway models that may train on your prompts from model listings",
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createAutoScroll } from "@kilocode/kilo-ui/hooks"
import { useSession } from "../../context/session"
import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
import { useConfig } from "../../context/config"
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
import { useProvider } from "../../context/provider"
import { useWorktreeMode } from "../../context/worktree-mode"
Expand Down Expand Up @@ -81,7 +82,7 @@ import {
parseProviderAuthError,
unwrapError,
} from "../../utils/errorUtils"
import type { Part, QuestionRequest, SuggestionRequest, ToolState } from "../../types/messages"
import type { Part, PromptRailPosition, QuestionRequest, SuggestionRequest, ToolState } from "../../types/messages"

interface MessageListProps {
onSelectSession?: (id: string) => void
Expand All @@ -104,6 +105,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
const session = useSession()
const server = useServer()
const language = useLanguage()
const config = useConfig()
const provider = useProvider()
const i18n = useI18n()
const data = useData()
Expand Down Expand Up @@ -1034,14 +1036,15 @@ export const MessageList: Component<MessageListProps> = (props) => {
window.addEventListener("scrollToMessage", onScrollToMessage)
onCleanup(() => window.removeEventListener("scrollToMessage", onScrollToMessage))

// Prompt rail: one tick per user prompt, positioned to the left of the
// Prompt rail: one tick per user prompt, positioned on the edge of the
// readable lane, opening a card of prompt/answer previews on hover.
const items = createMemo(() => promptItems(rows()))
// Until the transcript is measured there is no height to cap against, and
// rendering every prompt would spill ticks past the rail on long sessions.
const entries = createMemo(() => railEntries(items(), capacity(height()), session.hasOlderMessages()))
const [activeTurn, setActiveTurn] = createSignal<string>()
const railActiveKey = createMemo(() => items().find((item) => item.turn === activeTurn())?.key)
const railPosition = createMemo<PromptRailPosition>(() => config.config().prompt_rail_position ?? "left")

const [seek, setSeek] = createSignal<{ sid: string; count: number }>()
let paging = false
Expand Down Expand Up @@ -1371,6 +1374,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
</div>

<PromptRail
position={railPosition}
entries={entries}
items={items}
active={() => railActiveKey()}
Expand Down
34 changes: 26 additions & 8 deletions packages/kilo-vscode/webview-ui/src/components/chat/PromptRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* PromptRail component
* Thin vertical summary rail on the left edge of the transcript. Hovering or
* Thin vertical summary rail on the edge of the transcript. Hovering or
* focusing opens a bounded navigator for every loaded prompt; clicking jumps
* the virtualized transcript without mounting the intervening rows.
*/
Expand All @@ -15,8 +15,10 @@ import { Portal } from "solid-js/web"
import { VList, type VListHandle } from "virtua/solid"
import { useLanguage } from "../../context/language"
import { RAIL_INSET, ROW_HEIGHT, TICK_MIN, TICK_STEP, type PromptRailEntry, type PromptRailItem } from "./prompt-rail"
import type { PromptRailPosition } from "../../types/messages"

interface PromptRailProps {
position: Accessor<PromptRailPosition>
entries: Accessor<PromptRailEntry[]>
items: Accessor<PromptRailItem[]>
/** Row key of the item whose turn is currently at the top of the transcript. */
Expand Down Expand Up @@ -47,7 +49,7 @@ export function PromptRail(props: PromptRailProps) {
const [open, setOpen] = createSignal(false)
const [hover, setHover] = createSignal<string>()
const [focused, setFocused] = createSignal<number>()
const [anchor, setAnchor] = createSignal<{ top: number; left: number; height: number }>()
const [anchor, setAnchor] = createSignal<{ top: number; left?: number; right?: number; height: number }>()
let rail: HTMLElement | undefined
let card: HTMLDivElement | undefined
let list: VListHandle | undefined
Expand Down Expand Up @@ -86,6 +88,12 @@ export function PromptRail(props: PromptRailProps) {
// and the card never rides up over the task header or down over the composer.
// Before the card is mounted its height is estimated from the row count; the
// measured value takes over on the next frame, inside the fade-in.
//
// The side is anchored physically so the card always opens inward, away from
// the panel edge: only one of left/right is ever set, because a fixed element
// given both and no width stretches to span the gap between them. This math
// is physical in both directions, which is why .prompt-rail places itself
// with physical left/right instead of logical insets.
const place = () => {
if (!rail) return
const rect = rail.getBoundingClientRect()
Expand All @@ -96,9 +104,11 @@ export function PromptRail(props: PromptRailProps) {
const min = Math.max(EDGE, rect.top + 4)
const max = Math.min(window.innerHeight - EDGE, rect.bottom - 4) - height
const center = rect.top + rect.height / 2 - height / 2
const isRight = props.position() === "right"

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: Right-edge placement mixes logical CSS with physical card math, which breaks RTL

ar/fa set document.documentElement.dir = "rtl". The new right-edge CSS uses inset-inline-end, so choosing Right puts the rail on the physical left. This place() path then sets right: window.innerWidth - rect.left + GAP; with the rail already on the left that value is roughly 100vw and the hover card is pushed off-screen. Tick transform-origin: right and prompt-rail-in-right have the same physical assumption.

Suggestion: use physical left/right in CSS to match the setting labels, or keep logical insets and derive the card side from getBoundingClientRect() (open toward the larger gap).


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

setAnchor({
top: max < min ? min : Math.min(Math.max(center, min), max),
left: rect.right + GAP,
left: isRight ? undefined : rect.right + GAP,
right: isRight ? window.innerWidth - rect.left + GAP : undefined,
height: limit,
})
}
Expand Down Expand Up @@ -252,6 +262,16 @@ export function PromptRail(props: PromptRailProps) {
props.onLatest()
}

const cardStyle = (position: { top: number; left?: number; right?: number; height: number }) => {
const style: Record<string, string> = {
top: `${position.top}px`,
"--prompt-rail-card-height": `${position.height}px`,
}
if (position.left !== undefined) style.left = `${position.left}px`
if (position.right !== undefined) style.right = `${position.right}px`
return style
}

const row = (item: PromptRailItem, index: Accessor<number>) => (
<button
type="button"
Expand Down Expand Up @@ -279,6 +299,7 @@ export function PromptRail(props: PromptRailProps) {
<nav
ref={rail}
class="prompt-rail"
data-position={props.position()}
aria-label={language.t("session.prompts.navLabel")}
style={{ "--prompt-rail-step": `${step()}px` }}
onMouseLeave={closeCard}
Expand Down Expand Up @@ -328,14 +349,11 @@ export function PromptRail(props: PromptRailProps) {
<div
ref={card}
class="prompt-rail-card"
data-position={props.position()}
data-virtualized={virtualized() || undefined}
role="dialog"
aria-label={language.t("session.prompts.navLabel")}
style={{
top: `${position().top}px`,
left: `${position().left}px`,
"--prompt-rail-card-height": `${position().height}px`,
}}
style={cardStyle(position())}
onMouseEnter={cancelClose}
onMouseLeave={closeCard}
onWheel={(event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Switch } from "@kilocode/kilo-ui/switch"
import { useConfig } from "../../context/config"
import { useDisplay } from "../../context/display"
import { useLanguage } from "../../context/language"
import type { CodeEditDisplay, McpToolDisplay, TerminalCommandDisplay } from "../../types/messages"
import type { CodeEditDisplay, McpToolDisplay, PromptRailPosition, TerminalCommandDisplay } from "../../types/messages"
import SettingsRow from "./SettingsRow"

interface LayoutOption {
Expand All @@ -29,6 +29,11 @@ const MCP_OPTIONS: LayoutOption[] = [
{ value: "collapsed", labelKey: "settings.display.mcpTool.collapsed" },
]

const PROMPT_RAIL_OPTIONS: LayoutOption[] = [
{ value: "left", labelKey: "settings.display.promptRailPosition.left" },
{ value: "right", labelKey: "settings.display.promptRailPosition.right" },
]

const DisplayTab: Component = () => {
const { config, updateConfig, settings, updateSetting } = useConfig()
const display = useDisplay()
Expand Down Expand Up @@ -167,7 +172,6 @@ const DisplayTab: Component = () => {
<SettingsRow
title={language.t("settings.display.mcpTool.title")}
description={language.t("settings.display.mcpTool.description")}
last
>
<Select
options={MCP_OPTIONS}
Expand All @@ -185,6 +189,28 @@ const DisplayTab: Component = () => {
triggerVariant="settings"
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.display.promptRailPosition.title")}
description={language.t("settings.display.promptRailPosition.description")}
last
>
<Select
options={PROMPT_RAIL_OPTIONS}
current={PROMPT_RAIL_OPTIONS.find((o) => o.value === (config().prompt_rail_position ?? "left"))}
value={(o) => o.value}
label={(o) => language.t(o.labelKey)}
onSelect={(o) => {
if (!o) return
const next = o.value as PromptRailPosition
if (next === (config().prompt_rail_position ?? "left")) return
updateConfig({ prompt_rail_position: next })
}}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
</Card>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const KNOWN_KEYS: ReadonlyArray<string> = [
"terminal_command_display",
"code_edit_display",
"mcp_tool_display",
"prompt_rail_position",
"hide_prompt_training_models",
"sandbox",
"indexing",
Expand Down
5 changes: 5 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/br.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/bs.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/da.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,12 @@ export const dict = {
"settings.display.autoApprovalReason.description":
"Zeigt bei Tool-Aufrufen eine Zeile an, die erklärt, warum sie automatisch genehmigt wurden (passende Regel, Agent-Standard, YOLO-Modus usw.).",

"settings.display.promptRailPosition.title": "Position der Prompt-Navigation",
"settings.display.promptRailPosition.description":
"Wählen Sie, an welchem Rand des Chats die Leiste der Prompt-Navigation angezeigt wird.",
"settings.display.promptRailPosition.left": "Links",
"settings.display.promptRailPosition.right": "Rechts",

"chat.throughput.tooltip":
"Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.",
"chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.",
Expand Down
5 changes: 5 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,11 @@ export const dict = {
"settings.display.autoApprovalReason.title": "Show Auto-Approval Reason",
"settings.display.autoApprovalReason.description":
"Show a line on tool calls explaining why they were auto-approved (matched rule, agent default, YOLO mode, etc.).",
"settings.display.promptRailPosition.title": "Prompt Navigator Position",
"settings.display.promptRailPosition.description":
"Choose which edge of the chat the prompt navigator rail appears on.",
"settings.display.promptRailPosition.left": "Left",
"settings.display.promptRailPosition.right": "Right",

"chat.throughput.tooltip":
"Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.",
Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/es.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/fa.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/fr.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/it.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ja.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading