feat: session to terminal sync // new cli kilo theme

This commit is contained in:
Catriel Müller
2026-05-27 14:01:54 -03:00
parent 01ac265646
commit 7fc3bc09d4
11 changed files with 937 additions and 162 deletions
+76 -2
View File
@@ -9,13 +9,18 @@ import type {
ConfigSourcesResponse,
FormatterStatusResponse,
GlobalHealthResponse,
GlobalEvent,
LspStatusResponse,
McpStatusResponse,
Pty as PtyInfo,
PermissionRequest,
Project as KiloProject,
QuestionRequest,
ProviderAuthAuthorization,
ProviderAuthResponse,
ProviderListResponse,
Session as KiloSession,
SessionStatus,
ToolIdsResponse,
ToolListResponse,
TuiConfigGetResponse,
@@ -55,7 +60,11 @@ export type ProjectDiffItem = WorktreeDiffItem
export type ProjectPtyInfo = PtyInfo
export type ProjectTerminalItem = ProjectPtyInfo & {
directory: string
session?: KiloSession
sessionStatus?: SessionStatus
attention?: "permission" | "question"
}
export type ProjectConsoleEvent = GlobalEvent
export type Snapshot = {
health: GlobalHealthResponse
@@ -339,8 +348,46 @@ export async function resetProjectWorktree(input: Query, dir: string) {
export async function loadProjectTerminals(input: ProjectQuery, dir: string): Promise<ProjectTerminalItem[]> {
const sdk = client({ url: input.url, dir })
const result = await sdk.pty.list({ directory: dir })
return demand("Terminals", result).map((item) => ({ ...item, directory: dir }))
const [result, statusResult, permissionResult, questionResult] = await Promise.all([
sdk.pty.list({ directory: dir }),
maybe("Session status", sdk.session.status({ directory: dir })),
maybe("Pending permissions", sdk.permission.list({ directory: dir })),
maybe("Pending questions", sdk.question.list({ directory: dir })),
])
const rows = demand("Terminals", result)
const status = statusResult ?? {}
const permissions = pending(permissionResult ?? [])
const questions = pending(questionResult ?? [])
const sessions = new Map<string, KiloSession>()
await Promise.all(
rows.map(async (item) => {
if (!item.sessionID || sessions.has(item.sessionID)) return
const session = await maybe("Session", sdk.session.get({ directory: dir, sessionID: item.sessionID }))
if (session) sessions.set(item.sessionID, session)
}),
)
return rows.map((item) => {
const session = item.sessionID ? sessions.get(item.sessionID) : undefined
return {
...item,
directory: dir,
session,
sessionStatus: item.sessionID ? status[item.sessionID] : undefined,
attention: item.sessionID ? attention(item.sessionID, permissions, questions) : undefined,
}
})
}
function pending(items: Array<PermissionRequest | QuestionRequest>) {
return new Set(items.map((item) => item.sessionID))
}
function attention(id: string, permissions: Set<string>, questions: Set<string>) {
if (permissions.has(id)) return "permission"
if (questions.has(id)) return "question"
return undefined
}
export async function loadProjectDiff(input: Query, dir: string): Promise<ProjectDiffItem[]> {
@@ -361,6 +408,33 @@ export async function createProjectPty(input: Query, dir: string, title = "Kilo
return demand("Create terminal", result)
}
export async function removeProjectPty(input: Query, pty: string) {
const sdk = client({ url: input.url, dir: input.dir })
const result = await sdk.pty.remove({ directory: input.dir, ptyID: pty })
return demand("Remove terminal", result)
}
export async function viewProjectSessions(input: ProjectQuery, focused: string[], open: string[]) {
const sdk = client(input)
const result = await sdk.session.viewed({ directory: input.dir, focused, open })
return demand("Viewed sessions", result)
}
export function subscribeProjectEvents(input: ProjectQuery, handler: (event: ProjectConsoleEvent) => void) {
const sdk = client(input)
const ctl = new AbortController()
void (async () => {
const events = await sdk.global.event({ signal: ctl.signal, sseMaxRetryAttempts: 0 })
for await (const event of events.stream) {
if (ctl.signal.aborted) return
handler(event)
}
})().catch((err) => {
if (!ctl.signal.aborted) console.warn(`Project events: ${message(err)}`)
})
return () => ctl.abort()
}
export async function resizeProjectPty(input: Query, pty: string, cols: number, rows: number) {
const sdk = client({ url: input.url, dir: input.dir })
const result = await sdk.pty.update({ directory: input.dir, ptyID: pty, size: { cols, rows } })
@@ -1,6 +1,7 @@
import { A, useLocation, useParams } from "@solidjs/router"
import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, Show } from "solid-js"
import { Card } from "@kilocode/kilo-web-ui/card"
import { Icon } from "@kilocode/kilo-web-ui/icon"
import {
createProjectPty,
createProjectWorktree,
@@ -11,9 +12,13 @@ import {
loadProjectConsole,
loadProjectDiff,
loadProjectDiffFile,
removeProjectPty,
removeProjectWorktree,
resetProjectWorktree,
saveCached,
subscribeProjectEvents,
viewProjectSessions,
type ProjectConsoleEvent,
type ProjectConsoleQuery,
type ProjectTerminalItem,
type Query,
@@ -53,6 +58,51 @@ function title(input: string) {
return friendly(repo(input))
}
function record(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null
}
function nested(input: unknown, key: string) {
if (!record(input)) return undefined
const value = input[key]
if (!record(value)) return undefined
return value
}
function eventSession(event: ProjectConsoleEvent) {
const payload = event.payload
const props: unknown = "properties" in payload ? payload.properties : "data" in payload ? payload.data : undefined
if (!record(props)) return undefined
const id = props["sessionID"]
if (typeof id === "string") return id
const info = nested(props, "info")
if (typeof info?.["sessionID"] === "string") return info["sessionID"]
if (typeof info?.["id"] === "string") return info["id"]
const part = nested(props, "part")
if (typeof part?.["sessionID"] === "string") return part["sessionID"]
return undefined
}
function eventType(event: ProjectConsoleEvent) {
const payload = event.payload
if (payload.type !== "sync") return payload.type
return payload.name
}
function messageEvent(event: ProjectConsoleEvent) {
return eventType(event).startsWith("message.")
}
function refreshEvent(event: ProjectConsoleEvent) {
const type = eventType(event)
if (type.startsWith("pty.")) return true
if (type.startsWith("session.")) return true
if (type.startsWith("permission.")) return true
if (type.startsWith("question.")) return true
if (type.startsWith("message.")) return true
return false
}
export function ProjectConsoleRoute() {
const loc = useLocation()
const params = useParams()
@@ -65,6 +115,9 @@ export function ProjectConsoleRoute() {
const [file, setFile] = createSignal<string | undefined>()
const [saving, setSaving] = createSignal<string | undefined>()
const [failure, setFailure] = createSignal<string | undefined>()
const [unread, setUnread] = createSignal(new Set<string>())
const [labelRev, setLabelRev] = createSignal(0)
const events = { timer: undefined as number | undefined }
const project = () => params.project ?? ""
const query = createMemo<ProjectConsoleQuery | undefined>(() => {
const target = clean(url()) || fallback()
@@ -83,10 +136,10 @@ export function ProjectConsoleRoute() {
})
const terminals = createMemo(() => {
const items = new Map<string, ProjectTerminalItem>()
for (const item of snap()?.terminals ?? []) {
for (const item of local()) {
if (item.status === "running") items.set(item.id, item)
}
for (const item of local()) {
for (const item of snap()?.terminals ?? []) {
if (item.status === "running") items.set(item.id, item)
}
return Array.from(items.values())
@@ -139,6 +192,74 @@ export function ProjectConsoleRoute() {
return `/projects/${encodeURIComponent(project())}/settings${q ? `?${q}` : ""}`
})
function projectInput(): Query | undefined {
const base = query()
const data = snap()
if (!base || !data) return undefined
return { url: base.url, dir: data.project.worktree, scope: "project" }
}
function labelKey(dir: string) {
return `kilo.console.${project()}.worktree.${encodeURIComponent(dir)}.label`
}
function displayLabel(item: Context) {
labelRev()
return window.localStorage.getItem(labelKey(item.dir))?.trim() || item.label
}
function currentLabel() {
const item = current()
if (!item) return "Project"
return displayLabel(item)
}
function sessionID(item: ProjectTerminalItem | undefined) {
return item?.sessionID ?? item?.session?.id
}
function activeSessionID() {
return sessionID(activeTerminal())
}
function terminalName(item: ProjectTerminalItem) {
return item.session?.title || item.title || `Terminal ${item.id.slice(-4)}`
}
function terminalState(item: ProjectTerminalItem) {
if (item.attention) return "attention"
if (item.sessionStatus && item.sessionStatus.type !== "idle") return "busy"
const id = sessionID(item)
if (id && unread().has(id)) return "unread"
return "idle"
}
function clearUnread(item: ProjectTerminalItem) {
const id = sessionID(item)
if (!id || !unread().has(id)) return
setUnread((old) => {
const next = new Set(old)
next.delete(id)
return next
})
}
function markUnread(id: string) {
if (id === activeSessionID()) return
setUnread((old) => {
if (old.has(id)) return old
return new Set([...old, id])
})
}
function scheduleRefetch() {
if (events.timer) return
events.timer = window.setTimeout(() => {
events.timer = undefined
void refetch()
}, 150)
}
function terminalsFor(dir: string) {
return grouped().get(dir) ?? []
}
@@ -164,6 +285,7 @@ export function ProjectConsoleRoute() {
setSelected(item.directory)
setActive(item.id)
setFile(undefined)
clearUnread(item)
remember(item.directory, item.id)
}
@@ -177,21 +299,21 @@ export function ProjectConsoleRoute() {
}
function addWorktree() {
const input = target()
const input = projectInput()
const data = snap()
if (!input || !data) return
const name = window.prompt("Worktree name") ?? undefined
run("Creating worktree", async () => {
const next = await createProjectWorktree({ ...input, dir: data.project.worktree }, name)
const next = await createProjectWorktree(input, name)
setSelected(next.directory)
window.localStorage.setItem(`kilo.console.${project()}.dir`, next.directory)
})
}
function addSession() {
const input = target()
const item = current()
if (!input || !item) return
function addSession(item = current()) {
const base = query()
if (!base || !item) return
const input = { url: base.url, dir: item.dir, scope: "project" as const }
const label = `Kilo ${terminalsFor(item.dir).length + 1}`
setSaving("Creating session")
setFailure(undefined)
@@ -208,34 +330,65 @@ export function ProjectConsoleRoute() {
.finally(() => setSaving(undefined))
}
function dropTerminal(id: string) {
function forgetTerminal(id: string) {
setLocal((rows) => rows.filter((row) => row.id !== id))
if (active() === id) {
setActive("")
window.localStorage.removeItem(`kilo.console.${project()}.pty`)
}
}
function dropTerminal(id: string) {
forgetTerminal(id)
void refetch()
}
function removeSelected() {
const input = target()
const item = current()
const data = snap()
if (!input || !item || !data || item.kind === "local") return
if (!window.confirm(`Remove worktree ${item.label}?`)) return
run("Removing worktree", async () => {
await removeProjectWorktree({ ...input, dir: data.project.worktree }, item.dir)
setSelected(data.project.worktree)
function closeTerminal(item: ProjectTerminalItem) {
const input = { url: query()?.url ?? "", dir: item.directory, scope: "project" as const }
if (!input.url) return
run("Closing terminal", async () => {
await removeProjectPty(input, item.id)
forgetTerminal(item.id)
})
}
function resetSelected() {
const input = target()
function renameWorktree(item: Context) {
if (item.kind === "local") return
const input = window.prompt("Worktree label", displayLabel(item))
if (input === null) return
const next = input.trim()
if (next) window.localStorage.setItem(labelKey(item.dir), next)
else window.localStorage.removeItem(labelKey(item.dir))
setLabelRev((value) => value + 1)
}
function removeWorktree(item: Context) {
const input = projectInput()
if (!input || item.kind === "local") return
if (!window.confirm(`Remove worktree ${displayLabel(item)}?`)) return
run("Removing worktree", async () => {
await removeProjectWorktree(input, item.dir)
window.localStorage.removeItem(labelKey(item.dir))
setLabelRev((value) => value + 1)
if (selected() === item.dir) {
setSelected(input.dir)
remember(input.dir)
}
})
}
function removeSelected() {
const item = current()
const data = snap()
if (!input || !item || !data || item.kind === "local") return
if (!window.confirm(`Reset worktree ${item.label}?`)) return
run("Resetting worktree", async () => resetProjectWorktree({ ...input, dir: data.project.worktree }, item.dir))
if (!item) return
removeWorktree(item)
}
function resetSelected() {
const input = projectInput()
const item = current()
if (!input || !item || item.kind === "local") return
if (!window.confirm(`Reset worktree ${displayLabel(item)}?`)) return
run("Resetting worktree", async () => resetProjectWorktree(input, item.dir))
}
createEffect(() => {
@@ -287,6 +440,41 @@ export function ProjectConsoleRoute() {
remember(item.dir, pty?.id)
})
createEffect(() => {
const item = activeTerminal()
if (item) clearUnread(item)
})
createEffect(() => {
const base = query()
const data = snap()
if (!base || !data) return
const focused = activeSessionID()
const open = terminals().flatMap((item) => {
const id = sessionID(item)
return id ? [id] : []
})
void viewProjectSessions({ url: base.url, dir: data.project.worktree }, focused ? [focused] : [], open).catch(() => {})
})
createEffect(() => {
const base = query()
const data = snap()
if (!base || !data) return
const dirs = new Set([data.project.worktree, ...data.worktrees])
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)
if (refreshEvent(event)) scheduleRefetch()
})
onCleanup(stop)
})
onCleanup(() => {
if (events.timer) window.clearTimeout(events.timer)
})
createEffect(() => {
if (!snap.error || !discoverable(search())) return
const cached = loadCached()
@@ -315,45 +503,116 @@ export function ProjectConsoleRoute() {
</div>
<div class="project-console-scroll">
<section class="project-sidebar-group">
<div class="project-panel-heading">Actions</div>
<div class="project-console-actions">
<button type="button" onClick={addWorktree} disabled={!target() || !!saving()}>
New worktree
</button>
<button type="button" onClick={addSession} disabled={!target() || !!saving()}>
New session
<div class="project-panel-heading project-panel-heading-row">
<span>Worktrees</span>
<button
type="button"
class="project-heading-action"
onClick={addWorktree}
disabled={!projectInput() || !!saving()}
title="Create worktree"
aria-label="Create worktree"
>
+
</button>
</div>
</section>
<section class="project-sidebar-group">
<div class="project-panel-heading">Worktrees</div>
<nav class="project-contexts" aria-label="Worktrees">
<For each={contexts()}>
{(item) => (
<div class="project-worktree-block">
<button
type="button"
class="project-context"
classList={{ active: current()?.dir === item.dir && !activeTerminal() }}
onClick={() => select(item)}
title={item.dir}
>
<span>{item.label}</span>
<small>{item.kind === "local" ? "project" : repo(item.dir)}</small>
</button>
<div class="project-context-row" classList={{ active: current()?.dir === item.dir && !activeTerminal() }}>
<button
type="button"
class="project-context"
classList={{ active: current()?.dir === item.dir && !activeTerminal() }}
onClick={() => select(item)}
title={item.dir}
>
<span>{displayLabel(item)}</span>
<small>{item.kind === "local" ? "project" : repo(item.dir)}</small>
</button>
<div class="project-row-actions">
<button
type="button"
class="project-inline-action"
onClick={(event) => {
event.stopPropagation()
addSession(item)
}}
disabled={!query() || !!saving()}
title={`New session in ${displayLabel(item)}`}
aria-label={`New session in ${displayLabel(item)}`}
>
+
</button>
<Show when={item.kind === "worktree"}>
<button
type="button"
class="project-inline-action"
onClick={(event) => {
event.stopPropagation()
renameWorktree(item)
}}
disabled={!!saving()}
title={`Rename ${displayLabel(item)}`}
aria-label={`Rename ${displayLabel(item)}`}
>
<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>
</div>
</div>
<Show when={terminalsFor(item.dir).length > 0}>
<div class="project-terminal-list">
<For each={terminalsFor(item.dir)}>
{(pty) => (
<button
type="button"
class="project-terminal-row"
classList={{ active: active() === pty.id }}
onClick={() => selectTerminal(pty)}
title={`${pty.title} (${pty.pid})`}
>
<span>{pty.title || `Terminal ${pty.id.slice(-4)}`}</span>
</button>
<div class="project-terminal-item" classList={{ active: active() === pty.id }}>
<button
type="button"
class="project-terminal-row"
classList={{ active: active() === pty.id }}
onClick={() => selectTerminal(pty)}
title={`${terminalName(pty)} (${pty.pid})`}
>
<span
class="project-console-dot"
classList={{
"project-console-dot-idle": terminalState(pty) === "idle",
"project-console-dot-busy": terminalState(pty) === "busy",
"project-console-dot-attention": terminalState(pty) === "attention",
"project-console-dot-unread": terminalState(pty) === "unread",
}}
aria-hidden="true"
/>
<span>{terminalName(pty)}</span>
</button>
<button
type="button"
class="project-terminal-close"
onClick={(event) => {
event.stopPropagation()
closeTerminal(pty)
}}
disabled={!!saving()}
title={`Close ${terminalName(pty)}`}
aria-label={`Close ${terminalName(pty)}`}
>
x
</button>
</div>
)}
</For>
</div>
@@ -400,7 +659,7 @@ export function ProjectConsoleRoute() {
<Show when={!terminal() && !snap.loading && !snap.error && !failure()}>
<div class="project-terminal-empty">
<strong>No terminal session selected</strong>
<span>Use New session to start Kilo CLI in this worktree.</span>
<span>Use + next to a worktree to start Kilo CLI.</span>
</div>
</Show>
</main>
@@ -408,7 +667,7 @@ export function ProjectConsoleRoute() {
<aside class="project-console-info" aria-label="Project details">
<div class="project-info-card">
<div class="project-panel-heading">Context</div>
<strong>{current()?.label ?? "Project"}</strong>
<strong>{currentLabel()}</strong>
<code>{current()?.dir ?? snap()?.project.worktree ?? project()}</code>
<Show when={current()?.kind === "worktree"}>
<div class="project-info-actions">
@@ -15,6 +15,8 @@
}
.kilo-console .project-console-sidebar {
--project-action-rail: 5rem;
display: flex;
flex-direction: column;
height: 100%;
@@ -99,6 +101,7 @@
.kilo-console .project-panel-heading {
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 1.25rem;
color: var(--text-weaker);
font-size: 0.625rem;
@@ -108,6 +111,17 @@
text-transform: uppercase;
}
.kilo-console .project-panel-heading-row {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--project-action-rail);
gap: 0.25rem;
padding-right: 0;
}
.kilo-console .project-panel-heading-row .project-heading-action {
justify-self: end;
}
.kilo-console .project-console-sidebar button,
.kilo-console .project-settings-link {
display: flex;
@@ -143,6 +157,99 @@
opacity: 0.55;
}
.kilo-console .project-console-sidebar .project-heading-action,
.kilo-console .project-console-sidebar .project-inline-action,
.kilo-console .project-console-sidebar .project-terminal-close {
justify-content: center;
flex: 0 0 auto;
width: auto;
min-width: 1.35rem;
min-height: 1.35rem;
padding: 0 0.35rem;
color: var(--text-weaker);
font-size: 0.625rem;
}
.kilo-console .project-console-sidebar .project-inline-action {
max-width: 3.5rem;
}
.kilo-console .project-console-sidebar .project-inline-action [data-component="icon"] {
color: currentColor;
}
.kilo-console .project-console-sidebar .project-inline-action.danger:hover,
.kilo-console .project-console-sidebar .project-inline-action.danger:focus-visible {
color: var(--destructive, #ef4444);
}
.kilo-console .project-context-row,
.kilo-console .project-terminal-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.25rem;
border-radius: var(--radius-sm);
}
.kilo-console .project-context-row:hover,
.kilo-console .project-context-row:focus-within,
.kilo-console .project-context-row.active,
.kilo-console .project-terminal-item:hover,
.kilo-console .project-terminal-item:focus-within,
.kilo-console .project-terminal-item.active {
background: var(--muted);
color: var(--foreground);
}
.kilo-console .project-context-row .project-context:hover,
.kilo-console .project-context-row .project-context:focus-visible,
.kilo-console .project-context-row .project-context.active,
.kilo-console .project-context-row .project-inline-action:hover,
.kilo-console .project-context-row .project-inline-action:focus-visible,
.kilo-console .project-terminal-item .project-terminal-row:hover,
.kilo-console .project-terminal-item .project-terminal-row:focus-visible,
.kilo-console .project-terminal-item .project-terminal-row.active,
.kilo-console .project-terminal-item .project-terminal-close:hover,
.kilo-console .project-terminal-item .project-terminal-close:focus-visible {
background: transparent;
}
.kilo-console .project-context-row:hover .project-context,
.kilo-console .project-context-row:focus-within .project-context,
.kilo-console .project-context-row.active .project-context {
color: var(--foreground);
}
.kilo-console .project-terminal-item:hover .project-terminal-row,
.kilo-console .project-terminal-item:focus-within .project-terminal-row,
.kilo-console .project-terminal-item.active .project-terminal-row {
color: var(--foreground) !important;
}
.kilo-console .project-context-row .project-context {
min-width: 0;
}
.kilo-console .project-row-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 0.125rem;
width: var(--project-action-rail);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.kilo-console .project-context-row:hover .project-row-actions,
.kilo-console .project-context-row:focus-within .project-row-actions,
.kilo-console .project-terminal-item:hover .project-terminal-close,
.kilo-console .project-terminal-item:focus-within .project-terminal-close {
opacity: 1;
pointer-events: auto;
}
.kilo-console .project-context span,
.kilo-console .project-terminal-row span,
.kilo-console .project-settings-link span,
@@ -166,12 +273,41 @@
}
.kilo-console .project-terminal-row {
display: grid !important;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.375rem;
min-height: 1.5rem !important;
color: var(--muted-foreground) !important;
font-size: 0.6875rem !important;
font-weight: 400 !important;
}
.kilo-console .project-terminal-close {
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.kilo-console .project-terminal-row .project-console-dot-idle {
background: var(--muted-foreground);
opacity: 0.6;
}
.kilo-console .project-terminal-row .project-console-dot-busy {
background: var(--primary);
box-shadow: 0 0 0 0.1875rem color-mix(in oklab, var(--primary) 18%, transparent);
}
.kilo-console .project-terminal-row .project-console-dot-attention {
background: #f97316;
box-shadow: 0 0 0 0.1875rem color-mix(in oklab, #f97316 18%, transparent);
}
.kilo-console .project-terminal-row .project-console-dot-unread {
background: #22c55e;
box-shadow: 0 0 0 0.1875rem color-mix(in oklab, #22c55e 18%, transparent);
}
.kilo-console .project-terminal-row.active {
color: var(--foreground) !important;
}
@@ -17,6 +17,7 @@ import github from "./theme/github.json" with { type: "json" }
import gruvbox from "./theme/gruvbox.json" with { type: "json" }
import kanagawa from "./theme/kanagawa.json" with { type: "json" }
import kilo from "./theme/kilo.json" with { type: "json" } // kilocode_change
import kilo1 from "./theme/kilo-v1.json" with { type: "json" } // kilocode_change
import material from "./theme/material.json" with { type: "json" }
import matrix from "./theme/matrix.json" with { type: "json" }
import mercury from "./theme/mercury.json" with { type: "json" }
@@ -102,6 +103,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
gruvbox,
kanagawa,
kilo, // kilocode_change
["kilo-v1"]: kilo1, // kilocode_change
material,
matrix,
mercury,
@@ -0,0 +1,245 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#1e1e1e",
"darkStep2": "#252526",
"darkStep3": "#2d2d30",
"darkStep4": "#333333",
"darkStep5": "#3c3c3c",
"darkStep6": "#4a4a4a",
"darkStep7": "#3c3c3c",
"darkStep8": "#007fd4",
"darkStep9": "#faf74f",
"darkStep10": "#fbf86f",
"darkStep11": "#858585",
"darkStep12": "#cccccc",
"darkSecondary": "#007acc",
"darkAccent": "#007fd4",
"darkRed": "#f48771",
"darkOrange": "#cca700",
"darkGreen": "#89d185",
"darkCyan": "#3794ff",
"darkYellow": "#faf74f",
"lightStep1": "#ffffff",
"lightStep2": "#fafafa",
"lightStep3": "#f5f5f5",
"lightStep4": "#ebebeb",
"lightStep5": "#e1e1e1",
"lightStep6": "#d4d4d4",
"lightStep7": "#cecece",
"lightStep8": "#0090f1",
"lightStep9": "#616161",
"lightStep10": "#717171",
"lightStep11": "#717171",
"lightStep12": "#616161",
"lightSecondary": "#007acc",
"lightAccent": "#0090f1",
"lightRed": "#a1260d",
"lightOrange": "#bf8803",
"lightGreen": "#388a34",
"lightCyan": "#1a85ff",
"lightYellow": "#616161"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"accent": {
"dark": "darkAccent",
"light": "lightAccent"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#1a2a30",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#2a1a24",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#152328",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#23151c",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"markdownEmph": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"markdownStrong": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkAccent",
"light": "lightAccent"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -1,49 +1,45 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#1e1e1e",
"darkStep2": "#252526",
"darkStep3": "#2d2d30",
"darkStep4": "#333333",
"darkStep5": "#3c3c3c",
"darkStep6": "#4a4a4a",
"darkStep7": "#3c3c3c",
"darkStep8": "#007fd4",
"darkStep9": "#faf74f",
"darkStep10": "#fbf86f",
"darkStep11": "#858585",
"darkStep12": "#cccccc",
"darkSecondary": "#007acc",
"darkAccent": "#007fd4",
"darkRed": "#f48771",
"darkBase": "#0c0a09",
"darkPanel": "#1c1917",
"darkElement": "#292524",
"darkBorderSubtle": "#292524",
"darkBorder": "#44403b",
"darkBorderActive": "#f9f76f",
"darkText": "#fafaf9",
"darkTextMuted": "#a6a09b",
"darkTextWeaker": "#79716b",
"darkPrimary": "#f9f76f",
"darkPrimaryForeground": "#0c0a09",
"darkSecondary": "#a6a09b",
"darkAccent": "#f9f76f",
"darkRed": "#ff6467",
"darkOrange": "#cca700",
"darkGreen": "#89d185",
"darkCyan": "#3794ff",
"darkYellow": "#faf74f",
"lightStep1": "#ffffff",
"lightStep2": "#fafafa",
"lightStep3": "#f5f5f5",
"lightStep4": "#ebebeb",
"lightStep5": "#e1e1e1",
"lightStep6": "#d4d4d4",
"lightStep7": "#cecece",
"lightStep8": "#0090f1",
"lightStep9": "#616161",
"lightStep10": "#717171",
"lightStep11": "#717171",
"lightStep12": "#616161",
"lightSecondary": "#007acc",
"lightAccent": "#0090f1",
"lightRed": "#a1260d",
"lightOrange": "#bf8803",
"lightBase": "#ffffff",
"lightPanel": "#fafaf9",
"lightElement": "#f5f5f4",
"lightBorderSubtle": "#e7e5e4",
"lightBorder": "#d6d3d1",
"lightBorderActive": "#6f6500",
"lightText": "#0c0a09",
"lightTextMuted": "#79716b",
"lightTextWeaker": "#57534d",
"lightPrimary": "#6f6500",
"lightPrimaryForeground": "#ffffff",
"lightSecondary": "#57534d",
"lightAccent": "#6f6500",
"lightRed": "#e7000b",
"lightOrange": "#8a6d00",
"lightGreen": "#388a34",
"lightCyan": "#1a85ff",
"lightYellow": "#616161"
"lightCyan": "#1a85ff"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"secondary": {
"dark": "darkSecondary",
@@ -70,96 +66,104 @@
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
"dark": "darkText",
"light": "lightText"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
"dark": "darkTextMuted",
"light": "lightTextMuted"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
"dark": "darkBase",
"light": "lightBase"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
"dark": "darkPanel",
"light": "lightPanel"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
"dark": "darkElement",
"light": "lightElement"
},
"backgroundMenu": {
"dark": "darkPanel",
"light": "lightPanel"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
"dark": "darkBorder",
"light": "lightBorder"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
"dark": "darkBorderActive",
"light": "lightBorderActive"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
"dark": "darkBorderSubtle",
"light": "lightBorderSubtle"
},
"selectedListItemText": {
"dark": "darkPrimaryForeground",
"light": "lightPrimaryForeground"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
"dark": "darkGreen",
"light": "lightGreen"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
"dark": "darkRed",
"light": "lightRed"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
"dark": "darkTextMuted",
"light": "lightTextMuted"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
"light": "#2f7d32"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
"dark": "#ff8587",
"light": "#c00009"
},
"diffAddedBg": {
"dark": "#1a2a30",
"light": "#d5e5d5"
"dark": "#122318",
"light": "#e8f5e9"
},
"diffRemovedBg": {
"dark": "#2a1a24",
"light": "#f7d8db"
"dark": "#2a1214",
"light": "#ffe4e6"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
"dark": "darkPanel",
"light": "lightPanel"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
"dark": "darkTextWeaker",
"light": "lightTextWeaker"
},
"diffAddedLineNumberBg": {
"dark": "#152328",
"light": "#c5d5c5"
"dark": "#17291b",
"light": "#d7ecd8"
},
"diffRemovedLineNumberBg": {
"dark": "#23151c",
"light": "#e7c8cb"
"dark": "#35181a",
"light": "#ffd4d8"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
"dark": "darkText",
"light": "lightText"
},
"markdownHeading": {
"dark": "darkYellow",
"light": "lightYellow"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"markdownLinkText": {
"dark": "darkCyan",
@@ -170,52 +174,52 @@
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkSecondary",
"light": "lightSecondary"
"dark": "darkTextMuted",
"light": "lightTextMuted"
},
"markdownEmph": {
"dark": "darkSecondary",
"light": "lightSecondary"
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownStrong": {
"dark": "darkYellow",
"light": "lightYellow"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
"dark": "darkBorder",
"light": "lightBorder"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
"dark": "darkPrimary",
"light": "lightPrimary"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
"dark": "darkText",
"light": "lightText"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
"dark": "darkTextMuted",
"light": "lightTextMuted"
},
"syntaxKeyword": {
"dark": "darkAccent",
"light": "lightAccent"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxVariable": {
"dark": "darkRed",
@@ -238,8 +242,9 @@
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
"dark": "darkText",
"light": "lightText"
},
"thinkingOpacity": 0.72
}
}
@@ -67,10 +67,27 @@ export function useSessionEffects(deps: {
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
}) {
const pty = process.env.KILO_PTY_ID
const state = { prev: "" }
// Notify server which session the user is viewing
createEffect(() => {
const sessionID = deps.route.data.type === "session" ? deps.route.data.sessionID : undefined
deps.sdk.client.session.viewed({ focused: sessionID ? [sessionID] : [] }).catch(() => {})
if (!pty) return
const session = sessionID ? deps.sync.session.get(sessionID) : undefined
const key = [sessionID ?? "", session?.title ?? ""].join("\n")
if (key === state.prev) return
state.prev = key
deps.sdk.client.pty
.update({
ptyID: pty,
sessionID: sessionID ?? null,
...(session?.title ? { title: session.title } : {}),
})
.catch(() => {})
})
// Evict per-session data from store when navigating away
+13 -2
View File
@@ -13,6 +13,7 @@ import { PtyID } from "./schema"
import { Effect, Layer, Context, Schema, Types } from "effect"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
import { SessionID } from "@/session/schema" // kilocode_change
const log = Log.create({ service: "pty" })
@@ -63,6 +64,7 @@ export const Info = Schema.Struct({
cwd: Schema.String,
status: Schema.Literals(["running", "exited"]),
pid: PositiveInt,
sessionID: Schema.optional(Schema.NullOr(SessionID)), // kilocode_change
})
.annotate({ identifier: "Pty" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
@@ -81,6 +83,7 @@ export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInpu
export const UpdateInput = Schema.Struct({
title: Schema.optional(Schema.String),
sessionID: Schema.optional(Schema.NullOr(SessionID)), // kilocode_change
size: Schema.optional(
Schema.Struct({
rows: PositiveInt,
@@ -176,14 +179,16 @@ export const layer = Layer.effect(
const create = Effect.fn("Pty.create")(function* (input: CreateInput) {
const s = yield* InstanceState.get(state)
// kilocode_change start
const bridge = yield* EffectBridge.make()
const cfg = yield* config.get()
const id = PtyID.ascending()
// kilocode_change end
const resolved = KiloPtySelfCommand.resolve(input) // kilocode_change
const command = resolved.command || Shell.preferred(cfg.shell)
const args = resolved.args || []
const args = resolved.args || [] // kilocode_change
if (Shell.login(command)) {
args.push("-l")
args.push("-l") // kilocode_change
}
const cwd = resolved.cwd || s.dir // kilocode_change
@@ -194,6 +199,7 @@ export const layer = Layer.effect(
...shell.env,
TERM: "xterm-256color",
KILO_TERMINAL: "1",
KILO_PTY_ID: id, // kilocode_change
} as Record<string, string>
// kilocode_change start
// Don't leak the kilo server's auth credential into user shells.
@@ -283,6 +289,11 @@ export const layer = Layer.effect(
if (input.title) {
session.info.title = input.title
}
// kilocode_change start
if ("sessionID" in input) {
session.info.sessionID = input.sessionID ?? undefined
}
// kilocode_change end
if (input.size) {
session.process.resize(input.size.cols, input.size.rows)
}
+2
View File
@@ -1347,6 +1347,7 @@ export class Pty extends HeyApiClient {
directory?: string
workspace?: string
title?: string
sessionID?: string | null
size?: {
rows: number
cols: number
@@ -1363,6 +1364,7 @@ export class Pty extends HeyApiClient {
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "title" },
{ in: "body", key: "sessionID" },
{ in: "body", key: "size" },
],
},
+2
View File
@@ -688,6 +688,7 @@ export type Pty = {
cwd: string
status: "running" | "exited"
pid: number
sessionID?: string | null
}
export type EventPtyCreated = {
@@ -3319,6 +3320,7 @@ export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses]
export type PtyUpdateData = {
body?: {
title?: string
sessionID?: string | null
size?: {
rows: number
cols: number
+22
View File
@@ -1335,6 +1335,17 @@
"title": {
"type": "string"
},
"sessionID": {
"anyOf": [
{
"type": "string",
"pattern": "^ses.*"
},
{
"type": "null"
}
]
},
"size": {
"type": "object",
"properties": {
@@ -15081,6 +15092,17 @@
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"sessionID": {
"anyOf": [
{
"type": "string",
"pattern": "^ses.*"
},
{
"type": "null"
}
]
}
},
"required": ["id", "title", "command", "args", "cwd", "status", "pid"]