Skip to content

Commit c7564bc

Browse files
committed
feat(tui): overhaul phases 1-2 — icon system + custom OpenAI-compatible endpoints; fix model catalog
PHASE 1 (icons): central ICON registry (cli/src/utils/icons.ts) replaces ~160 scattered emoji with a coherent single-width Unicode glyph set (text-presentation-safe, themeable). Swept: activity bar, welcome screen, status marks, toasts, attachment/image cards, session/marketplace/init/ pr-swarm/login output, dialogs. Fixed welcome-footer literal \u00B7 rendering as raw text and its stale hardcoded theme name. PHASE 2 (provider UX): the wizard now supports Custom OpenAI-Compatible endpoints — base URL, optional display name, optional API key (auth-less local servers), manual model IDs, connection test against YOUR endpoint, unique per-endpoint id, baseUrl persisted. updateProvider store action added. Schema supported baseUrl/customModelIds all along; the UI never exposed them (wizard filtered the custom category and never wrote baseUrl). FIX: parseModelCatalog iterated provider-level keys instead of the nested provider.models map (models.dev shape), producing junk entries like id 'models' that the picker offered — selecting one bricked every run. Now parses the nested map (7,560 entries live, 0 junk). Model routing also guards against placeholder API keys (e.g. 'test' from env auto-detection), falling through to the next provider instead of an opaque upstream 401. Plan + full audit: docs/ui-overhaul-plan.md. cli 2391 tests green; tsc green across cli/common.
1 parent 8559e69 commit c7564bc

36 files changed

Lines changed: 510 additions & 134 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to LevelCode will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased] - TUI Overhaul Phases 1-2 (2026-09-05)
9+
10+
### Added
11+
- **Icon system (Phase 1)** — central `ICON` registry (`cli/src/utils/icons.ts`) replaces ~160 scattered emoji with a coherent single-width Unicode glyph set (text-presentation-safe), themed via tokens. Activity bar, welcome screen, status marks, toasts, attachment/image cards, session/marketplace/init/pr-swarm/login output, dialogs all swept. Fixed the literal `·` rendering as raw text on the welcome footer (and dropped its stale hardcoded theme name).
12+
- **Custom OpenAI-compatible endpoints (Phase 2)** — the provider wizard now offers Custom > Custom OpenAI-Compatible. Full flow: base URL (LM Studio, llama.cpp, vLLM, LiteLLM, Ollama /v1, any gateway) -> optional display name -> API key (optional for auth-less local servers) -> manual model IDs (comma-separated, for endpoints without /models) -> connection test against YOUR endpoint (not the static definition URL) -> saved with a unique per-endpoint id and the baseUrl persisted. `updateProvider` action added to the provider store. Full audit + phased plan: docs/ui-overhaul-plan.md.
13+
14+
### Fixed
15+
- **Model catalog produced junk entries**`parseModelCatalog` iterated provider-level keys instead of the nested `provider.models` map (models.dev shape), yielding entries like id "models" per provider; the picker offered garbage and selecting it bricked all runs. Now parses the nested map (7,560 real entries verified live, zero junk) and never treats the container as a model.
16+
- **Placeholder API keys routed requests to doomed providers** — env auto-detection can seed keys like "test"; model routing now requires a usable key and falls through to the next-priority provider instead of producing an opaque upstream 401.
17+
818
## [Unreleased] - Session Inspection, Fork Points & Swarm Budgets (2026-09-01)
919

1020
### Added

cli/src/agents-console/agents-console.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type { TeamConfig, TeamMember, TeamTask } from '@levelcode/common/types/team-config'
11+
import { ICON } from '../utils/icons'
1112

1213
export type ComplianceTailEntry = {
1314
eventType: string
@@ -242,7 +243,7 @@ export function formatTeamDetail(
242243
const status = theme[statusColor[task.status]](pad(task.status, 12))
243244
const owner = task.owner ? theme.dim(` @${task.owner}`) : theme.dim(' (unowned)')
244245
const blockedBy =
245-
task.blockedBy.length > 0 ? theme.red(` blocked by ${task.blockedBy.join(', ')}`) : ''
246+
task.blockedBy.length > 0 ? theme.red(` blocked by ${task.blockedBy.join(', ')}`) : ''
246247
lines.push(` #${pad(task.id, 5)} ${status} ${task.subject}${owner}${blockedBy}`)
247248
}
248249

cli/src/chat.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import type { FileTreeNode } from '@levelcode/common/util/file'
109109
import type { KeyEvent, ScrollBoxRenderable } from '@opentui/core'
110110
import type { UseMutationResult } from '@tanstack/react-query'
111111
import type { Dispatch, SetStateAction } from 'react'
112+
import { ICON } from './utils/icons'
112113

113114
export const Chat = ({
114115
headerContent,
@@ -1409,7 +1410,7 @@ export const Chat = ({
14091410
if (queuePreviewTitle) {
14101411
segments.push(queuePreviewTitle)
14111412
} else if (pausedQueueText) {
1412-
segments.push(` ${pausedQueueText}`)
1413+
segments.push(` ${pausedQueueText}`)
14131414
}
14141415

14151416
if (segments.length === 0) {

cli/src/cli-main.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { setOscDetectedTheme } from './utils/theme-system'
4646
import type { AgentMode } from './utils/constants'
4747
import type { FileTreeNode } from '@levelcode/common/util/file'
4848
import type { ParsedArgs } from './types/cli-args'
49+
import { ICON } from './utils/icons'
4950

5051
// Configure TanStack Query's focusManager for terminal environments
5152
// This is required because there's no browser visibility API in terminal apps
@@ -204,7 +205,7 @@ async function runCli(): Promise<void> {
204205
const result = await handlePublish(agentIds)
205206

206207
if (result.success && result.publisherId && result.agents) {
207-
logger.info(green(' Successfully published:'))
208+
logger.info(green(' Successfully published:'))
208209
for (const agent of result.agents) {
209210
logger.info(
210211
cyan(
@@ -214,7 +215,7 @@ async function runCli(): Promise<void> {
214215
}
215216
process.exit(0)
216217
} else {
217-
logger.error(red(' Publish failed'))
218+
logger.error(red(' Publish failed'))
218219
if (result.error) logger.error(red(`Error: ${result.error}`))
219220
if (result.details) logger.error(red(result.details))
220221
if (result.hint) logger.warn(yellow(`Hint: ${result.hint}`))

cli/src/commands/__tests__/init.test.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('handleInitializationFlowLocally', () => {
6868
// Check message indicates creation
6969
const messages = postUserMessage([])
7070
expect(messages.length).toBeGreaterThan(0)
71-
expect(getMessageText(messages)).toContain(' Created `knowledge.md`')
71+
expect(getMessageText(messages)).toContain(' Created `knowledge.md`')
7272
})
7373

7474
test('skips knowledge.md creation when it already exists', () => {
@@ -86,7 +86,7 @@ describe('handleInitializationFlowLocally', () => {
8686

8787
// Check message indicates file already exists
8888
const messages = postUserMessage([])
89-
expect(getMessageText(messages)).toContain('📋 `knowledge.md` already exists')
89+
expect(getMessageText(messages)).toContain(' `knowledge.md` already exists')
9090
})
9191
})
9292

@@ -102,7 +102,7 @@ describe('handleInitializationFlowLocally', () => {
102102
)
103103

104104
const messages = postUserMessage([])
105-
expect(getMessageText(messages)).toContain(' Created `.agents/`')
105+
expect(getMessageText(messages)).toContain(' Created `.agents/`')
106106
})
107107

108108
test('skips .agents directory creation when it already exists', () => {
@@ -119,7 +119,7 @@ describe('handleInitializationFlowLocally', () => {
119119
expect(agentsDirCalls.length).toBe(0)
120120

121121
const messages = postUserMessage([])
122-
expect(getMessageText(messages)).toContain('📋 `.agents/` already exists')
122+
expect(getMessageText(messages)).toContain(' `.agents/` already exists')
123123
})
124124
})
125125

@@ -135,7 +135,7 @@ describe('handleInitializationFlowLocally', () => {
135135
)
136136

137137
const messages = postUserMessage([])
138-
expect(getMessageText(messages)).toContain(' Created `.agents/types/`')
138+
expect(getMessageText(messages)).toContain(' Created `.agents/types/`')
139139
})
140140

141141
test('skips .agents/types directory creation when it already exists', () => {
@@ -156,7 +156,7 @@ describe('handleInitializationFlowLocally', () => {
156156
expect(typesDirCalls.length).toBe(0)
157157

158158
const messages = postUserMessage([])
159-
expect(getMessageText(messages)).toContain('📋 `.agents/types/` already exists')
159+
expect(getMessageText(messages)).toContain(' `.agents/types/` already exists')
160160
})
161161
})
162162

@@ -212,7 +212,7 @@ describe('handleInitializationFlowLocally', () => {
212212

213213
const messages = postUserMessage([])
214214
expect(getMessageText(messages)).toContain(
215-
'📋 `.agents/types/agent-definition.ts` already exists',
215+
' `.agents/types/agent-definition.ts` already exists',
216216
)
217217
})
218218
})
@@ -272,7 +272,7 @@ describe('handleInitializationFlowLocally', () => {
272272
const messageContent = getMessageText(messages)
273273

274274
// Should have error message for tools.ts
275-
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/tools.ts`')
275+
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/tools.ts`')
276276
expect(messageContent).toContain('Permission denied')
277277
})
278278

@@ -333,12 +333,12 @@ describe('handleInitializationFlowLocally', () => {
333333
const messageContent = getMessageText(messages)
334334

335335
// Should have error for agent-definition.ts
336-
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/agent-definition.ts`')
336+
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/agent-definition.ts`')
337337
expect(messageContent).toContain('File locked')
338338

339339
// But should still succeed for tools.ts and util-types.ts
340-
expect(messageContent).toContain(' Copied `.agents/types/tools.ts`')
341-
expect(messageContent).toContain(' Copied `.agents/types/util-types.ts`')
340+
expect(messageContent).toContain(' Copied `.agents/types/tools.ts`')
341+
expect(messageContent).toContain(' Copied `.agents/types/util-types.ts`')
342342
})
343343

344344
test('handles non-Error exceptions in type file copying', () => {
@@ -355,7 +355,7 @@ describe('handleInitializationFlowLocally', () => {
355355
const messageContent = getMessageText(messages)
356356

357357
// Should handle non-Error exceptions gracefully
358-
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/util-types.ts`')
358+
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/util-types.ts`')
359359
expect(messageContent).toContain('string error')
360360
})
361361

@@ -373,7 +373,7 @@ describe('handleInitializationFlowLocally', () => {
373373
const messageContent = getMessageText(messages)
374374

375375
// Should handle null exceptions with 'Unknown' fallback
376-
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/tools.ts`')
376+
expect(messageContent).toContain('⚠ Failed to copy `.agents/types/tools.ts`')
377377
expect(messageContent).toContain('Unknown')
378378
})
379379
})
@@ -420,9 +420,9 @@ describe('handleInitializationFlowLocally', () => {
420420
const messages = postUserMessage([])
421421
const messageContent = getMessageText(messages)
422422

423-
expect(messageContent).toContain('📋 `knowledge.md` already exists')
424-
expect(messageContent).toContain('📋 `.agents/` already exists')
425-
expect(messageContent).toContain(' Created `.agents/types/`')
423+
expect(messageContent).toContain(' `knowledge.md` already exists')
424+
expect(messageContent).toContain(' `.agents/` already exists')
425+
expect(messageContent).toContain(' Created `.agents/types/`')
426426
})
427427

428428
test('handles fully initialized project correctly', () => {
@@ -439,17 +439,17 @@ describe('handleInitializationFlowLocally', () => {
439439
const messageContent = getMessageText(messages)
440440

441441
// All messages should indicate existing files
442-
expect(messageContent).toContain('📋 `knowledge.md` already exists')
443-
expect(messageContent).toContain('📋 `.agents/` already exists')
444-
expect(messageContent).toContain('📋 `.agents/types/` already exists')
442+
expect(messageContent).toContain(' `knowledge.md` already exists')
443+
expect(messageContent).toContain(' `.agents/` already exists')
444+
expect(messageContent).toContain(' `.agents/types/` already exists')
445445
expect(messageContent).toContain(
446-
'📋 `.agents/types/agent-definition.ts` already exists',
446+
' `.agents/types/agent-definition.ts` already exists',
447447
)
448448
expect(messageContent).toContain(
449-
'📋 `.agents/types/tools.ts` already exists',
449+
' `.agents/types/tools.ts` already exists',
450450
)
451451
expect(messageContent).toContain(
452-
'📋 `.agents/types/util-types.ts` already exists',
452+
' `.agents/types/util-types.ts` already exists',
453453
)
454454
})
455455
})

cli/src/commands/command-registry.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ import type { SendMessageFn } from '../types/contracts/send-message'
141141
import type { User } from '../utils/auth'
142142
import type { AgentMode } from '../utils/constants'
143143
import type { UseMutationResult } from '@tanstack/react-query'
144+
import { ICON } from '../utils/icons'
144145

145146
export type RouterParams = {
146147
abortControllerRef: React.MutableRefObject<AbortController | null>
@@ -1676,7 +1677,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
16761677
params.setMessages((prev) => [
16771678
...prev,
16781679
getUserMessage(params.inputValue.trim()),
1679-
getSystemMessage(' Attaching swarm to PR...'),
1680+
getSystemMessage(' Attaching swarm to PR...'),
16801681
])
16811682
params.saveToHistory(params.inputValue.trim())
16821683
clearInput(params)
@@ -1688,7 +1689,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
16881689
return [...withoutPending, getSystemMessage(result)]
16891690
})
16901691
} catch (error) {
1691-
const msg = ` PR attach failed: ${error instanceof Error ? error.message : String(error)}`
1692+
const msg = ` PR attach failed: ${error instanceof Error ? error.message : String(error)}`
16921693
params.setMessages((prev) => {
16931694
const withoutPending = prev.slice(0, -1)
16941695
return [...withoutPending, getSystemMessage(msg)]
@@ -1781,7 +1782,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
17811782
params.setMessages((prev) => [
17821783
...prev,
17831784
getUserMessage(params.inputValue.trim()),
1784-
getSystemMessage(' Starting relay server...'),
1785+
getSystemMessage(' Starting relay server...'),
17851786
])
17861787
params.saveToHistory(params.inputValue.trim())
17871788
clearInput(params)
@@ -1792,7 +1793,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
17921793
return [...withoutPending, getSystemMessage(result)]
17931794
})
17941795
} catch (error) {
1795-
const msg = ` ${error instanceof Error ? error.message : String(error)}`
1796+
const msg = ` ${error instanceof Error ? error.message : String(error)}`
17961797
params.setMessages((prev) => {
17971798
const withoutPending = prev.slice(0, -1)
17981799
return [...withoutPending, getSystemMessage(msg)]
@@ -2339,7 +2340,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
23392340
clearInput(params)
23402341
params.setMessages((prev) => [
23412342
...prev,
2342-
getSystemMessage(' Checking MCP servers...'),
2343+
getSystemMessage(' Checking MCP servers...'),
23432344
])
23442345
try {
23452346
const { mcpServers } = loadMCPConfigSync({ verbose: false })
@@ -2518,7 +2519,7 @@ Change with /effort <level>. Applies to the next message.`),
25182519
params.setMessages((prev) => [
25192520
...prev,
25202521
getUserMessage(params.inputValue.trim()),
2521-
getSystemMessage(' Building code map...'),
2522+
getSystemMessage(' Building code map...'),
25222523
])
25232524
clearInput(params)
25242525
try {

cli/src/commands/init.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { trackEvent } from '../utils/analytics'
1515
import { getSystemMessage } from '../utils/message-history'
1616

1717
import type { PostUserMessageFn } from '../types/contracts/send-message'
18+
import { ICON } from '../utils/icons'
1819

1920
const INITIAL_KNOWLEDGE_FILE = `# Project knowledge
2021
@@ -58,10 +59,10 @@ export function handleInitializationFlowLocally(): {
5859
const messages: string[] = []
5960

6061
if (existsSync(knowledgePath)) {
61-
messages.push(`📋 \`${PRIMARY_KNOWLEDGE_FILE_NAME}\` already exists.`)
62+
messages.push(` \`${PRIMARY_KNOWLEDGE_FILE_NAME}\` already exists.`)
6263
} else {
6364
writeFileSync(knowledgePath, INITIAL_KNOWLEDGE_FILE)
64-
messages.push(` Created \`${PRIMARY_KNOWLEDGE_FILE_NAME}\``)
65+
messages.push(` Created \`${PRIMARY_KNOWLEDGE_FILE_NAME}\``)
6566

6667
// Track knowledge file creation
6768
trackEvent(AnalyticsEvent.KNOWLEDGE_FILE_UPDATED, {
@@ -75,23 +76,23 @@ export function handleInitializationFlowLocally(): {
7576
const agentsTypesDir = path.join(agentsDir, 'types')
7677

7778
if (existsSync(agentsDir)) {
78-
messages.push('📋 `.agents/` already exists.')
79+
messages.push(' `.agents/` already exists.')
7980
} else {
8081
mkdirSync(agentsDir, { recursive: true })
81-
messages.push(' Created `.agents/`')
82+
messages.push(' Created `.agents/`')
8283
}
8384

8485
if (existsSync(agentsTypesDir)) {
85-
messages.push('📋 `.agents/types/` already exists.')
86+
messages.push(' `.agents/types/` already exists.')
8687
} else {
8788
mkdirSync(agentsTypesDir, { recursive: true })
88-
messages.push(' Created `.agents/types/`')
89+
messages.push(' Created `.agents/types/`')
8990
}
9091

9192
for (const { fileName, source } of COMMON_TYPE_FILES) {
9293
const targetPath = path.join(agentsTypesDir, fileName)
9394
if (existsSync(targetPath)) {
94-
messages.push(`📋 \`.agents/types/${fileName}\` already exists.`)
95+
messages.push(` \`.agents/types/${fileName}\` already exists.`)
9596
continue
9697
}
9798

@@ -100,10 +101,10 @@ export function handleInitializationFlowLocally(): {
100101
throw new Error('Source content is empty')
101102
}
102103
writeFileSync(targetPath, source)
103-
messages.push(` Copied \`.agents/types/${fileName}\``)
104+
messages.push(` Copied \`.agents/types/${fileName}\``)
104105
} catch (error) {
105106
messages.push(
106-
`⚠ Failed to copy \`.agents/types/${fileName}\`: ${
107+
`⚠ Failed to copy \`.agents/types/${fileName}\`: ${
107108
error instanceof Error ? error.message : String(error ?? 'Unknown')
108109
}`,
109110
)

0 commit comments

Comments
 (0)