From 76d06fd8baaf256b2d7f5e5f93bc901c9a8b0b4e Mon Sep 17 00:00:00 2001 From: Johnny Amancio Date: Thu, 30 Jul 2026 21:23:50 +0200 Subject: [PATCH] fix: name the auto-approve commands by scope and repair CI failures Restore packages/ui/src/components/markdown.css. Upstream moved markdown styling into the new session-ui package and deleted this file, but Kilo still ships packages/ui's markdown component to the VS Code webview, so the rules went missing while their consumer stayed. The copy button lost position: absolute and opacity: 0, so it rendered below every code block instead of in its corner, and showed the copy and check icons at once because nothing hid the check state. The palette showed "Enable auto-approve permissions" next to "Enable auto-approve mode", which reads as the same command twice. They are not: one lasts for the TUI run and is what --auto and --yolo seed, the other saves a rule to global config. Both now say which they are and carry a desc, which the palette already renders. The saved one is enforced server side, so its desc says it covers every client. No behaviour change, and the command names are untouched so the slash aliases still work. Tests no longer depend on the developer's credential store. Credential imports Global.data/auth.json on startup, and the catalog, plugin and models-dev layers built Credential.node without overriding Global, so results changed depending on whether the machine was logged in. They now use a temp data directory, as the integration suite already did. That suite in turn set KILO_AUTH_CONTENT at module scope; bun shares one process across test files, so it leaked into credential.test.ts, whose auth.json cases then read "{}" and found no credentials. Scoped with beforeAll/afterAll. macOS-only failures, all merge gaps in Kilo tests: - sandbox/state.test.ts runs a script in a subprocess; the LayerNode migration put AppNodeBuilder.build in it without adding the import - sandbox/session-tools.test.ts mocked MCP without clients(), which upstream's resource tools now call - sandbox/session.test.ts built SessionV2 without binding SessionExecution Generated files: schema.gen.ts was formatted by an older prettier than the one main's dependency bump resolves, so the generator no longer reproduced it and the migration check called the schema stale. packages/client was missing Pty.sessionID and the compaction include field. Both regenerated. Also corrected the listener comment: it claimed application services still come from AppRuntime, but the SessionV2 and MoveSession builds sit inside KiloListener's Layer.fresh boundary and self-provide their subtrees, which is deliberate since both bind this listener's own LocationServiceMap. --- packages/client/src/generated/types.ts | 6 + packages/core/src/database/schema.gen.ts | 32 +- packages/core/test/catalog.test.ts | 30 +- packages/core/test/integration.test.ts | 14 +- packages/core/test/plugin/fixture.ts | 9 + packages/core/test/plugin/models-dev.test.ts | 11 +- .../opencode/src/kilocode/cli/cmd/tui/app.tsx | 8 +- .../server/routes/instance/httpapi/server.ts | 8 +- .../kilocode/sandbox/session-tools.test.ts | 1 + .../test/kilocode/sandbox/session.test.ts | 7 +- .../test/kilocode/sandbox/state.test.ts | 9 +- packages/tui/src/app.tsx | 14 +- packages/ui/src/components/markdown.css | 289 ++++++++++++++++++ packages/ui/src/styles/index.css | 1 + 14 files changed, 413 insertions(+), 26 deletions(-) create mode 100644 packages/ui/src/components/markdown.css diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 3b3188c874..68075b68e3 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -1090,6 +1090,7 @@ export type SessionsHistoryOutput = { readonly reason: "auto" | "manual" readonly text: string readonly recent: string + readonly include?: string | null } } | { @@ -1548,6 +1549,7 @@ export type SessionsEventsOutput = readonly reason: "auto" | "manual" readonly text: string readonly recent: string + readonly include?: string | undefined } } | { @@ -2561,6 +2563,7 @@ export type PtysListOutput = { readonly status: "running" | "exited" readonly pid: number readonly exitCode?: number + readonly sessionID?: string | null }> } @@ -2620,6 +2623,7 @@ export type PtysCreateOutput = { readonly status: "running" | "exited" readonly pid: number readonly exitCode?: number + readonly sessionID?: string | null } } @@ -2645,6 +2649,7 @@ export type PtysGetOutput = { readonly status: "running" | "exited" readonly pid: number readonly exitCode?: number + readonly sessionID?: string | null } } @@ -2675,6 +2680,7 @@ export type PtysUpdateOutput = { readonly status: "running" | "exited" readonly pid: number readonly exitCode?: number + readonly sessionID?: string | null } } diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index 8fe2132ebe..27c474ccbd 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -238,16 +238,32 @@ export default { `) yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) - yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`) - yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) - yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`) - yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`) - yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`) - yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) - yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) - yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 6c736cde1e..c6e9976735 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -1,8 +1,12 @@ -import { describe, expect } from "bun:test" +import { afterAll, describe, expect } from "bun:test" import { Effect, Fiber, Layer, Stream } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" +import fs from "node:fs" +import os from "node:os" +import path from "node:path" import { Credential } from "@opencode-ai/core/credential" +import { Global } from "@opencode-ai/core/global" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" @@ -23,9 +27,25 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") })), ) +// kilocode_change - Credential imports Global.data/auth.json on startup, so without this the suite +// reads the developer's real credential store and its results depend on whether they are logged in. +const dataDirs: string[] = [] +// Each Layer.fresh below rebuilds Credential, which re-imports data/auth.json, so a shared directory +// would carry credentials from one test into the next. Give every layer its own. +const globalLayer = () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kilo-catalog-test-")) + dataDirs.push(dir) + return Global.layerWith({ data: dir }) +} +afterAll(() => { + for (const dir of dataDirs) fs.rmSync(dir, { recursive: true, force: true }) +}) const catalogLayer = AppNodeBuilder.build( LayerNode.group([Catalog.node, EventV2.node, Credential.node, Integration.node, Policy.node]), - [[Location.node, locationLayer]], + [ + [Location.node, locationLayer], + [Global.node, globalLayer()], // kilocode_change + ], ) const it = testEffect(catalogLayer) @@ -48,7 +68,10 @@ describe("CatalogV2", () => { it.effect("derives availability from active credentials without changing provider state", () => { const integrationID = Integration.ID.make("test") const localCatalogLayer = Layer.fresh( - AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node]), [[Location.node, locationLayer]]), + AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node]), [ + [Location.node, locationLayer], + [Global.node, globalLayer()], // kilocode_change + ]), ) return Effect.gen(function* () { @@ -79,6 +102,7 @@ describe("CatalogV2", () => { const localCatalogLayer = Layer.fresh( AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [ [Location.node, locationLayer], + [Global.node, globalLayer()], // kilocode_change ]), ) diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index e88f388c93..09dc2ad070 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { afterAll, beforeAll, describe, expect } from "bun:test" import { Duration, Effect, Exit, Fiber, Scope, Stream } from "effect" import * as TestClock from "effect/testing/TestClock" import { Credential } from "@opencode-ai/core/credential" @@ -16,8 +16,18 @@ import { testEffect } from "./lib/effect" // them on every startup. testEffect rebuilds the layer per test, so without isolation one test's // credentials reappear in the next as "Imported" (and the real developer store gets written to). // KILO_AUTH_CONTENT is Kilo's process-local credential mode: it skips the auth.json dual-write. -process.env.KILO_AUTH_CONTENT ??= "{}" +// Set it around this file only. bun shares one process across test files, so setting it at module +// scope leaks into credential.test.ts, whose auth.json cases then read "{}" and see no credentials. const directory = fs.mkdtempSync(path.join(os.tmpdir(), "kilo-integration-test-")) +let previous: string | undefined +beforeAll(() => { + previous = process.env.KILO_AUTH_CONTENT + process.env.KILO_AUTH_CONTENT ??= "{}" +}) +afterAll(() => { + if (previous === undefined) delete process.env.KILO_AUTH_CONTENT + else process.env.KILO_AUTH_CONTENT = previous +}) const it = testEffect( AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, EventV2.node]), [ [Global.node, Global.layerWith({ data: directory })], diff --git a/packages/core/test/plugin/fixture.ts b/packages/core/test/plugin/fixture.ts index 0d5b52180a..8b1d9add78 100644 --- a/packages/core/test/plugin/fixture.ts +++ b/packages/core/test/plugin/fixture.ts @@ -15,9 +15,17 @@ import { Npm } from "@opencode-ai/core/npm" import { PluginV2 } from "@opencode-ai/core/plugin" import { Reference } from "@opencode-ai/core/reference" import { SkillV2 } from "@opencode-ai/core/skill" +import { Global } from "@opencode-ai/core/global" import { Effect, Layer } from "effect" +import fs from "node:fs" +import os from "node:os" +import path from "node:path" import { tempLocationLayer } from "../fixture/location" +// kilocode_change - Credential imports Global.data/auth.json on startup, so without this the suite +// reads the developer's real credential store and its results depend on whether they are logged in. +const globalLayer = Global.layerWith({ data: fs.mkdtempSync(path.join(os.tmpdir(), "kilo-plugin-test-")) }) + const npmLayer = Layer.succeed( Npm.Service, Npm.Service.of({ @@ -48,5 +56,6 @@ export const PluginTestLayer = AppNodeBuilder.build( [ [Location.node, tempLocationLayer], [Npm.node, npmLayer], + [Global.node, globalLayer], // kilocode_change ], ) diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 6f43a97cfa..2890260aab 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -1,5 +1,5 @@ import path from "path" -import { describe, expect } from "bun:test" +import { afterAll, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" @@ -14,6 +14,9 @@ import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev" import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "../fixture/location" +import { Global } from "@opencode-ai/core/global" +import fs from "node:fs" +import os from "node:os" import { testEffect } from "../lib/effect" import { catalogHost, host, integrationHost } from "./host" @@ -21,8 +24,14 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), ) +// kilocode_change - Catalog pulls Credential, which imports Global.data/auth.json on startup, so +// without this the suite reads the developer's real credential store. +const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "kilo-modelsdev-test-")) +const globalLayer = Global.layerWith({ data: dataDir }) +afterAll(() => fs.rmSync(dataDir, { recursive: true, force: true })) const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, EventV2.node]), [ [Location.node, locationLayer], + [Global.node, globalLayer], // kilocode_change ]) const it = testEffect(layer) diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx index 543750842f..a2ce6c8bd0 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx @@ -279,10 +279,12 @@ export function init() { name: "permission.allow_everything", get title() { return isAllowEverything(sync.data.config.permission) - ? "Disable auto-approve mode" - : "Enable auto-approve mode" + ? "Disable saved auto-approve" + : "Enable saved auto-approve" }, - desc: "Toggle auto-approve for all permission prompts, saved to global config", + // kilocode_change - the saved rule is server side, so it also stops VS Code, JetBrains and + // headless runs from prompting + desc: "Toggle auto-approve for all permission prompts, saved to global config and shared with every client", category: "System", slashName: "auto-approve", slashAliases: ["autoapprove", "approve-all", "approveall"], diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 0c7a8a4ce5..062890235e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -357,7 +357,13 @@ export function createListenerRoutes(corsOptions?: CorsOptions) { provideKiloListenerRoutes(corsOptions), // Upstream's v2 ServerApi groups declare location/session middleware and services that must be // satisfied when the layer is built, not at request time, so the listener needs the same chain - // createRoutes uses. Application services still come from AppRuntime. + // createRoutes uses. + // + // These builds sit inside KiloListener's Layer.fresh boundary, so each one self-provides its own + // dependency subtree rather than resolving AppRuntime's. That is deliberate: SessionV2 is bound + // to this listener's LocationServiceMap and to SessionExecutionLocal, so it cannot be the + // process-wide instance. Everything the graph does not rebind (the nodes listed in AppLayer) + // still comes from AppRuntime, and the scope teardown releases the rest. Layer.provide(sessionLocationLayer), Layer.provide(locationLayer), Layer.provide(PtyEnvironment.layer), diff --git a/packages/opencode/test/kilocode/sandbox/session-tools.test.ts b/packages/opencode/test/kilocode/sandbox/session-tools.test.ts index 77a39d4fd6..73b33c9227 100644 --- a/packages/opencode/test/kilocode/sandbox/session-tools.test.ts +++ b/packages/opencode/test/kilocode/sandbox/session-tools.test.ts @@ -112,6 +112,7 @@ const plugin = Layer.mock(Plugin.Service)({ }) const mcp = Layer.mock(MCP.Service)({ tools: () => Effect.succeed({}), + clients: () => Effect.succeed({}), // kilocode_change - upstream's MCP resource tools probe the clients }) const lsp = Layer.mock(LSP.Service)({ touchFile: () => Effect.void, diff --git a/packages/opencode/test/kilocode/sandbox/session.test.ts b/packages/opencode/test/kilocode/sandbox/session.test.ts index 72df9c2a30..713bb19d6c 100644 --- a/packages/opencode/test/kilocode/sandbox/session.test.ts +++ b/packages/opencode/test/kilocode/sandbox/session.test.ts @@ -7,6 +7,7 @@ import { Deferred, Effect, Exit, Layer } from "effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Database } from "@opencode-ai/core/database/database" import { SessionV2 } from "@opencode-ai/core/session" +import { SessionExecution } from "@opencode-ai/core/session/execution" import { BackgroundJob } from "@/background/job" import { Bus } from "@/bus" import { Config } from "@/config/config" @@ -37,7 +38,7 @@ const it = testEffect( Layer.provide(AppNodeBuilder.build(BackgroundJob.node)), Layer.provide(AppNodeBuilder.build(Database.node)), Layer.provide(AppNodeBuilder.build(EventV2Bridge.node)), - Layer.provide(AppNodeBuilder.build(SessionV2.node)), + Layer.provide(AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]])), // kilocode_change ), AppNodeBuilder.build(BackgroundJob.node), Bus.layer, @@ -138,7 +139,9 @@ describe("sandbox session cleanup", () => { if (!status.available) return const token = SandboxInheritance.issue({ sessionID: source.id, directory: dir, count: 1 }) - const child = yield* provideInstance(worktree)(sessions.create({ title: "sandbox-child", sandboxInheritanceToken: token })) + const child = yield* provideInstance(worktree)( + sessions.create({ title: "sandbox-child", sandboxInheritanceToken: token }), + ) expect((yield* provideInstance(worktree)(SandboxPolicy.status(child.id))).enabled).toBe(true) yield* provideInstance(dir)(SandboxPolicy.toggle(source.id)) diff --git a/packages/opencode/test/kilocode/sandbox/state.test.ts b/packages/opencode/test/kilocode/sandbox/state.test.ts index 44fa1fcb1a..eb6ed1cac6 100644 --- a/packages/opencode/test/kilocode/sandbox/state.test.ts +++ b/packages/opencode/test/kilocode/sandbox/state.test.ts @@ -19,7 +19,12 @@ import { TestInstance } from "../../fixture/fixture" import { testEffect } from "../../lib/effect" const it = testEffect( - Layer.mergeAll(Bus.layer, AppNodeBuilder.build(Config.node), AppNodeBuilder.build(CrossSpawnSpawner.node), AppNodeBuilder.build(Database.node)), + Layer.mergeAll( + Bus.layer, + AppNodeBuilder.build(Config.node), + AppNodeBuilder.build(CrossSpawnSpawner.node), + AppNodeBuilder.build(Database.node), + ), ) const linux = process.platform === "linux" ? test : test.skip const posix = process.platform === "win32" ? test.skip : test @@ -37,6 +42,7 @@ test("restores the session snapshot after a backend restart", async () => { 'import { Effect, Layer } from "effect"', 'import { Config } from "@/config/config"', 'import { Database } from "@opencode-ai/core/database/database"', + 'import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"', 'import { InstanceRef } from "@/effect/instance-ref"', 'import * as SandboxPolicy from "@/kilocode/sandbox/policy"', 'import { SandboxStore } from "@/kilocode/sandbox/store"', @@ -143,6 +149,7 @@ linux("reports configured network namespace availability", async () => { 'import { Effect, Layer } from "effect"', 'import { Config } from "@/config/config"', 'import { Database } from "@opencode-ai/core/database/database"', + 'import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"', 'import { InstanceRef } from "@/effect/instance-ref"', 'import * as SandboxPolicy from "@/kilocode/sandbox/policy"', 'import { SessionID } from "@/session/schema"', diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 3006605a01..70843b974d 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -954,14 +954,18 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi dialog.clear() }, }, - // kilocode_change - this toggles the in-memory mode that `--auto`/`--yolo` seed, and is the - // only way to leave it mid-session. Kilo also ships `permission.allow_everything` - // (kilocode/cli/cmd/tui/app.tsx), which persists a server-side allow rule instead. Two - // entries with different scope is confusing and should be consolidated onto the Kilo one. + // kilocode_change - titled by scope. This toggles the in-memory mode that `--auto`/`--yolo` + // seed, which lasts for the TUI process and survives switching sessions. It is also the only + // way to leave that mode without restarting; Kilo's `permission.allow_everything` + // (kilocode/cli/cmd/tui/app.tsx) saves a global rule instead. Consolidating the two is a + // follow-up that has to cover VS Code and JetBrains as well. { name: "permission.mode", title: - local.permission.mode === "auto" ? "Disable auto-approve permissions" : "Enable auto-approve permissions", + local.permission.mode === "auto" + ? "Disable auto-approve for this TUI run" + : "Enable auto-approve for this TUI run", + desc: "Auto-approve permission prompts until you exit the TUI, nothing is saved", // kilocode_change category: "System", run: () => { local.permission.toggle() diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css new file mode 100644 index 0000000000..ad25fbf272 --- /dev/null +++ b/packages/ui/src/components/markdown.css @@ -0,0 +1,289 @@ +[data-component="markdown"] { + /* Reset & Base Typography */ + min-width: 0; + max-width: 100%; + overflow-wrap: break-word; + color: var(--text-strong); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); /* 14px */ + line-height: 160%; + + /* Spacing for flow */ + > *:first-child { + margin-top: 0; + } + > *:last-child { + margin-bottom: 0; + } + > [data-markdown-block]:first-child > *:first-child { + margin-top: 0; + } + > [data-markdown-block]:last-child > *:last-child { + margin-bottom: 0; + } + + /* Headings: Same size, distinguished by color and spacing */ + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: 14px; + color: var(--text-strong); + font-weight: var(--font-weight-medium); + margin-top: 0px; + margin-bottom: 24px; + line-height: var(--line-height-large); + } + + /* Emphasis & Strong: Neutral strong color */ + strong, + b { + color: var(--text-strong); + font-weight: var(--font-weight-medium); + } + + /* Paragraphs */ + p { + margin-bottom: 12px; + } + + /* Links */ + a { + color: var(--text-interactive-base); + text-decoration: none; + font-weight: inherit; + } + + a:hover { + text-decoration: underline; + text-underline-offset: 2px; + } + + /* Lists */ + ul, + ol { + margin-top: 8px; + margin-bottom: 12px; + margin-inline-start: 0; /* kilocode_change */ + padding-inline-start: 32px; /* kilocode_change */ + list-style-position: outside; + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + padding-inline-start: 2.25rem; /* kilocode_change */ + } + + li { + margin-bottom: 8px; + } + + li > p:first-child { + display: inline; + margin: 0; + } + + li > p + p { + display: block; + margin-top: 0.5rem; + } + + li::marker { + color: var(--text-weak); + } + + /* Nested lists spacing */ + li > ul, + li > ol { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + padding-inline-start: 1rem; /* Minimal indent for nesting only */ /* kilocode_change */ + } + + li > ol { + padding-inline-start: 1.75rem; /* kilocode_change */ + } + + /* Blockquotes */ + blockquote { + border-inline-start: 2px solid var(--border-weak-base); /* kilocode_change */ + margin: 1.5rem 0; + padding-inline-start: 0.5rem; /* kilocode_change */ + color: var(--text-weak); + font-style: normal; + } + + /* Horizontal Rule - Invisible spacing only */ + hr { + border: none; + height: 0; + margin: 40px 0; + } + + .shiki { + background: var(--color-background-stronger); + color: var(--text-base); + font-size: 13px; + padding: 12px; + border-radius: 6px; + border: 0.5px solid var(--border-weak-base); + } + + [data-component="markdown-code"] { + position: relative; + } + + [data-slot="markdown-copy-button"] { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 1; + + &::after { + content: attr(data-tooltip); + position: absolute; + left: 50%; + bottom: calc(100% + 4px); + transform: translateX(-50%); + z-index: 1000; + + max-width: 320px; + border-radius: var(--radius-sm); + background: var(--surface-float-base); + color: var(--text-invert-strong); + padding: 2px 8px; + border: 1px solid var(--border-weak-base, rgba(0, 0, 0, 0.07)); + box-shadow: var(--shadow-md); + + pointer-events: none; + white-space: nowrap; + + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + + opacity: 0; + transition: opacity 0.15s ease; + } + } + + [data-slot="markdown-copy-button"]:hover::after, + [data-slot="markdown-copy-button"]:focus-visible::after { + opacity: 1; + } + + [data-slot="markdown-copy-button"][data-variant="secondary"] { + box-shadow: none; + border: 1px solid var(--border-weak-base); + } + + [data-slot="markdown-copy-button"][data-variant="secondary"] [data-slot="icon-svg"] { + color: var(--icon-base); + } + + [data-component="markdown-code"]:hover [data-slot="markdown-copy-button"] { + opacity: 1; + } + + [data-slot="markdown-copy-button"] [data-slot="check-icon"] { + display: none; + } + + [data-slot="markdown-copy-button"][data-copied="true"] [data-slot="copy-icon"] { + display: none; + } + + [data-slot="markdown-copy-button"][data-copied="true"] [data-slot="check-icon"] { + display: inline-flex; + } + + pre { + margin-top: 12px; + margin-bottom: 32px; + overflow: auto; + + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + } + + :not(pre) > code { + font-family: var(--font-family-mono); + font-feature-settings: var(--font-family-mono--font-feature-settings); + color: var(--syntax-string); + font-weight: var(--font-weight-medium); + /* font-size: 13px; */ + + /* padding: 2px 2px; */ + /* margin: 0 1.5px; */ + /* border-radius: 2px; */ + /* background: var(--surface-base); */ + /* box-shadow: 0 0 0 0.5px var(--border-weak-base); */ + + /* kilocode_change start */ + &.file-link { + cursor: pointer; + text-decoration-line: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; + transition: color 0.15s ease; + + &:hover { + color: var(--text-interactive-base); + text-decoration-style: solid; + } + } + /* kilocode_change end */ + } + + /* Tables */ + table { + width: 100%; + border-collapse: collapse; + margin: 24px 0; + font-size: var(--font-size-base); + display: block; + overflow-x: auto; + } + + th, + td { + /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */ + border-bottom: 1px solid var(--border-weaker-base); + padding: 12px; + text-align: start; /* kilocode_change */ + vertical-align: top; + } + + th { + color: var(--text-strong); + font-weight: var(--font-weight-medium); + border-bottom: 1px solid var(--border-weak-base); + } + + /* Images */ + img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 1.5rem 0; + display: block; + } +} + +[data-component="markdown"] a.external-link:hover > code { + text-decoration: underline; + text-underline-offset: 2px; +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 1cda2af5d2..f2216d01ce 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -33,6 +33,7 @@ @import "../components/inline-input.css" layer(components); @import "../components/list.css" layer(components); @import "../components/logo.css" layer(components); +@import "../components/markdown.css" layer(components); @import "../kilocode/markdown-mermaid.css" layer(components); /* kilocode_change */ @import "../components/message-part.css" layer(components); @import "../components/message-nav.css" layer(components);