Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions assets/hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions assets/social.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/adapters/agents/kilo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ambientUnset,
anthropicOptions,
collapsedTierWarning,
compatIgnoredWarning,
sessionUnavailableWarning,
extendedContextWarning,
modelRef,
Expand Down Expand Up @@ -57,7 +58,7 @@ export const kilo = {
},
binary,
translate(input: TranslateInput): Translation {
const { intent, passthrough } = input
const { intent, passthrough, profile } = input
const primary = intent.models.opus

const config: Record<string, unknown> = {
Expand All @@ -82,6 +83,8 @@ export const kilo = {
if (collapse) warnings.push(collapse)
const ext = extendedContextWarning(intent, primary, 'Kilo')
if (ext) warnings.push(ext)
const compat = compatIgnoredWarning(profile.compat, 'Kilo')
if (compat) warnings.push(compat)

return { plan: { set, unset: ambientUnset(intent) }, args: [...passthrough], warnings }
},
Expand Down
5 changes: 4 additions & 1 deletion src/adapters/agents/opencode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ambientUnset,
anthropicOptions,
collapsedTierWarning,
compatIgnoredWarning,
sessionUnavailableWarning,
extendedContextWarning,
modelRef,
Expand Down Expand Up @@ -56,7 +57,7 @@ export const opencode = {
},
binary,
translate(input: TranslateInput): Translation {
const { intent, passthrough } = input
const { intent, passthrough, profile } = input
const primary = intent.models.opus
const small = intent.models.haiku

Expand Down Expand Up @@ -87,6 +88,8 @@ export const opencode = {
if (collapse) warnings.push(collapse)
const ext = extendedContextWarning(intent, primary, 'OpenCode')
if (ext) warnings.push(ext)
const compat = compatIgnoredWarning(profile.compat, 'OpenCode')
if (compat) warnings.push(compat)

return { plan: { set, unset: ambientUnset(intent) }, args, warnings }
},
Expand Down
29 changes: 29 additions & 0 deletions src/adapters/agents/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ export function collapsedTierWarning(
}
}

/**
* Warn when a profile carries gateway compatibility flags that this agent does
* not consume.
*
* The compat mechanism is Claude-Code-only (capabilities.compatFlags). A flag
* toggled on for a Kilo or OpenCode setup changes nothing and, until this
* existed, said so NOWHERE — not at edit, not at launch, not in the doctor.
* That is the one capability mismatch with no net, and the
* capability-gap-never-silently-dropped rule the tier and session warnings
* already follow says it should have one. The flags are kept, not stripped, so
* the setup still works when it runs Claude Code again.
*/
export function compatIgnoredWarning(
compat: Record<string, boolean | undefined> | undefined,
agentLabel: string,
): EnvWarning | null {
const active = Object.entries(compat ?? {})
.filter(([, on]) => on)
.map(([id]) => id)
if (active.length === 0) return null
return {
severity: 'medium',
code: 'compat-ignored',
message:
`${agentLabel} does not use gateway compatibility flags; these are set but ignored: ` +
`${active.join(', ')}.`,
}
}

/**
* Warn when a 1M-capable provider is reached without Claude Code's `[1m]`
* signal, which only Claude Code sends. The model still runs; its window is
Expand Down
32 changes: 32 additions & 0 deletions test/adapters/agents/kilo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,35 @@ test('capabilities describe a single model slot', () => {
assert.equal(kilo.binary.name, 'kilo')
assert.equal(kilo.binary.overrideEnv, 'SWISSCODE_KILO_BIN')
})

test('a gateway compat flag on a Kilo profile is flagged as ignored — the safety net', () => {
// The UI now hides the compat section for Kilo, but a hand-edited or
// agent-switched config can still carry a flag. Compat is Claude-Code-only,
// so Kilo warns rather than silently doing nothing — the one mismatch that
// previously had no net at edit, launch, or doctor.
const input: TranslateInput = {
intent: intent(),
profile: makeProfile({ provider: 'zai', compat: { disableAdaptiveThinking: true } }),
provider: null,
passthrough: [],
ambient: {},
}
const w = kilo.translate(input).warnings.find((x) => x.code === 'compat-ignored')
assert.ok(w, 'Kilo should warn that a compat flag is ignored')
assert.match(w.message, /disableAdaptiveThinking/)
assert.match(w.message, /Kilo/)
})

test('a Kilo profile with no compat flags emits no compat-ignored warning', () => {
const input: TranslateInput = {
intent: intent(),
profile: makeProfile({ provider: 'zai' }),
provider: null,
passthrough: [],
ambient: {},
}
assert.equal(
kilo.translate(input).warnings.find((x) => x.code === 'compat-ignored'),
undefined,
)
})
9 changes: 9 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>swisscode</title>
<!--
The Swiss-cross mark, served same-origin from web/public/ (Vite copies it
to the asset root, swisscode's server hands it back as image/svg+xml). A
file rather than a data: URI so it is cacheable and the CSP stays 'self'.
-->
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<!-- Match the browser chrome to whichever theme is active. -->
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f5f5f7" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#08090a" />
<!--
The server rewrites this placeholder with a per-run token before serving.
A meta tag rather than a cookie: a cookie rides along on cross-origin
Expand Down
22 changes: 22 additions & 0 deletions web/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,28 @@ export default defineConfig({
default: { value: { base: '#cf222e', _dark: '#f85149' } },
subtle: { value: { base: 'rgba(207,34,46,0.10)', _dark: 'rgba(248,81,73,0.14)' } },
},

/**
* The swisscode identity, from the README hero, and RESERVED FOR IT.
*
* The red and coral are the brand — the logo mark and the "code" half
* of the wordmark. They are deliberately NOT the interactive accent:
* the README borrows GitHub's palette, where red is the identity and a
* separate blue carries interaction, and doing the same here keeps a
* brand-red button from reading as a danger button (the dark-mode
* danger and the coral are nearly the same hue).
*
* `mark` is the red HANDLE — a fill behind a white terminal prompt,
* NOT a cross (see the note in web/src/Brand.tsx on why the Swiss
* cross was retired). White-on-red reads on any surface, so it needs
* no light/dark variant. `wordmark` IS text, so light mode darkens the
* coral to clear the 3:1 large-text bar on a white panel (measured
* 4.3:1 at #d93a2b), while dark mode keeps the bright #ff5a4d (5:1).
*/
brand: {
mark: { value: '#da291c' },
wordmark: { value: { base: '#d93a2b', _dark: '#ff5a4d' } },
},
},
},

Expand Down
5 changes: 5 additions & 0 deletions web/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 2 additions & 6 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type ThemePreference,
} from './theme'
import { ApiError, api, type Bootstrap } from './api'
import { BrandMark } from './Brand'
import { Banner, Dot, SegmentedControl } from './ui'
import { Profiles } from './routes/Profiles'
import { Accounts } from './routes/Accounts'
Expand Down Expand Up @@ -140,12 +141,7 @@ export function App() {
})}
>
<div className={css({ px: '2', py: '2', mb: '3' })}>
<div className={css({ textStyle: 'body', fontWeight: 'title' })}>
swisscode
</div>
<div className={css({ textStyle: 'micro', color: 'content.tertiary', mt: '0.5' })}>
local configuration
</div>
<BrandMark subtitle="local configuration" />
</div>

<nav className={css({ display: 'flex', flexDirection: 'column', gap: '0.5' })}>
Expand Down
160 changes: 160 additions & 0 deletions web/src/Brand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// The swisscode identity, in the UI.
//
// The logo, the two-tone "swiss·code" wordmark, and a small custom empty-state
// graphic, matched to assets/hero.svg and rebuilt as theme-aware React so the
// web UI reads as the same product. Colour comes from the `brand.*` tokens,
// which are reserved for identity and never used for controls (see the note in
// panda.config.ts).
//
// WHY THE MARK IS A PROMPT, NOT A CROSS. A white cross on red is the Swiss flag
// and coat of arms — restricted for commercial use, and evocative of the
// Victorinox "Swiss Army Knife" marks. So the mark keeps the RED HANDLE (a
// colour is not protectable) but drops the cross for a terminal chevron: it
// reads as a prompt (a coding tool), as two blades opening from a pivot (the
// multi-tool homage the name is really about — many providers from one
// launcher), and as none of the protected things. Do not "restore the cross":
// its absence is the point.
//
// The one hardcoded colour here is the white of the chevron and cursor. It is
// not a theme value that slipped past strictTokens — white-on-red is intrinsic
// to the mark and holds on either theme. strictTokens governs `css()` props,
// not SVG fill/stroke attributes, so it is stated plainly.
import type { ReactNode } from 'react'
import { css } from '../styled-system/css'

/**
* The mark: a terminal prompt on a red tile.
*
* A chevron whose vertex is the pivot and whose two arms open like blades, plus
* a block cursor — `❯█`. One bold chevron and one block, so it stays legible
* from a 16px favicon to a 120px social card. The red tile is a fill behind
* white, so it needs no light/dark variant.
*/
export function Logo({ size = 22, title = 'swisscode' }: { size?: number; title?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
role="img"
aria-label={title}
className={css({ flexShrink: 0, display: 'block' })}
>
<rect width="100" height="100" rx="23" fill="var(--colors-brand-mark)" />
{/* the prompt: a chevron (pivot at its point, arms as blades) + a block cursor */}
<path
d="M30 34 L54 50 L30 66"
fill="none"
stroke="#ffffff"
strokeWidth="11"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect x="62" y="41" width="13" height="18" rx="3" fill="#ffffff" />
</svg>
)
}

/**
* "swiss" + "code" — the wordmark, with the coral half from the hero.
*
* A single element with two coloured spans rather than an image, so it inherits
* the page's font and stays crisp at any zoom. `weight: title` (590) matches the
* heading scale; the tracking comes from the textStyle.
*/
export function Wordmark({ style = 'heading' }: { style?: 'heading' | 'title' | 'display' }) {
return (
<span className={css({ textStyle: style, fontWeight: 'title', whiteSpace: 'nowrap' })}>
<span className={css({ color: 'content.primary' })}>swiss</span>
<span className={css({ color: 'brand.wordmark' })}>code</span>
</span>
)
}

/**
* Logo + wordmark + one line of context, as the sidebar header.
*
* The subtitle is `content.tertiary` so the identity carries the weight and the
* label recedes — the same hierarchy the README hero uses under its wordmark.
*/
export function BrandMark({ subtitle }: { subtitle?: string }) {
return (
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
<Logo size={24} />
<div className={css({ display: 'flex', flexDirection: 'column', gap: '0' })}>
<Wordmark />
{subtitle ? (
<span className={css({ textStyle: 'micro', color: 'content.tertiary', mt: '-0.5' })}>
{subtitle}
</span>
) : null}
</div>
</div>
)
}

/**
* The empty-state graphic.
*
* A dashed card holding a faint version of the prompt mark — "swisscode,
* nothing here yet" rather than a bare line of text. Drawn from border and
* brand tokens so it adapts to both themes; the mark sits at low opacity so it
* reads as a watermark, not a status. Paired with copy by the `Empty` primitive.
*/
export function EmptyGraphic({ size = 72 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
role="presentation"
aria-hidden="true"
className={css({ display: 'block' })}
>
<rect
x="10"
y="10"
width="80"
height="80"
rx="16"
fill="none"
stroke="var(--colors-border-strong)"
strokeWidth="2"
strokeDasharray="6 7"
/>
<g opacity="0.5">
<path
d="M36 38 L54 50 L36 62"
fill="none"
stroke="var(--colors-brand-mark)"
strokeWidth="8"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect x="60" y="44" width="10" height="13" rx="2.5" fill="var(--colors-brand-mark)" />
</g>
</svg>
)
}

/** The empty-state layout: the graphic above a line of copy, centred. */
export function EmptyState({ children }: { children: ReactNode }) {
return (
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '3',
py: '8',
px: '4',
textAlign: 'center',
})}
>
<EmptyGraphic />
<p className={css({ textStyle: 'body', color: 'content.tertiary', maxW: 'content' })}>
{children}
</p>
</div>
)
}
Loading