Skip to content

Commit dff3913

Browse files
brandonkachenclaude
andcommitted
Merge origin/main into brandon/cli-themes
Resolved conflicts by accepting main's refactoring changes. Main branch includes: - Refactored hooks architecture (use-agent-initialization, use-auth-state, etc.) - New tool components (diff-viewer, read-subtree, write-todos, etc.) - UI improvements and constants - Test additions Our daemon improvements remain intact in theme-system.ts and related files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
2 parents 16429c8 + a2d418d commit dff3913

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2550
-989
lines changed

bun.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@
287287
},
288288
"sdk": {
289289
"name": "@codebuff/sdk",
290-
"version": "0.5.12",
290+
"version": "0.6.1",
291291
"dependencies": {
292292
"@jitl/quickjs-wasmfile-release-sync": "0.31.0",
293293
"@vscode/tree-sitter-wasm": "0.1.4",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { OPEN_DELAY_MS, CLOSE_DELAY_MS, REOPEN_SUPPRESS_MS } from '../../components/agent-mode-toggle'
2+
3+
export const createHoverToggleControllerForTest = () => {
4+
let isOpen = false
5+
let reopenBlockedUntil = 0
6+
let openTimeout: any = null
7+
let closeTimeout: any = null
8+
9+
const clearOpenTimer = () => {
10+
clearTimeout(openTimeout)
11+
openTimeout = null
12+
}
13+
14+
const clearCloseTimer = () => {
15+
clearTimeout(closeTimeout)
16+
closeTimeout = null
17+
}
18+
19+
const clearAllTimers = () => {
20+
clearOpenTimer()
21+
clearCloseTimer()
22+
}
23+
24+
const openNow = () => {
25+
clearAllTimers()
26+
isOpen = true
27+
}
28+
29+
const closeNow = (suppressReopen = false) => {
30+
clearAllTimers()
31+
isOpen = false
32+
if (suppressReopen) {
33+
reopenBlockedUntil = Date.now() + REOPEN_SUPPRESS_MS
34+
}
35+
}
36+
37+
const scheduleOpen = () => {
38+
if (isOpen) return
39+
if (Date.now() < reopenBlockedUntil) return
40+
clearOpenTimer()
41+
openTimeout = setTimeout(() => {
42+
openNow()
43+
}, OPEN_DELAY_MS)
44+
}
45+
46+
const scheduleClose = () => {
47+
if (!isOpen) return
48+
clearCloseTimer()
49+
closeTimeout = setTimeout(() => {
50+
isOpen = false
51+
closeTimeout = null
52+
}, CLOSE_DELAY_MS)
53+
}
54+
55+
return {
56+
get isOpen() {
57+
return isOpen
58+
},
59+
openNow,
60+
closeNow,
61+
scheduleOpen,
62+
scheduleClose,
63+
clearOpenTimer,
64+
clearCloseTimer,
65+
clearAllTimers,
66+
}
67+
}
68+
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2+
3+
import { buildExpandedSegments, resolveAgentModeClick, OPEN_DELAY_MS, CLOSE_DELAY_MS, REOPEN_SUPPRESS_MS } from '../../components/agent-mode-toggle'
4+
import { createHoverToggleControllerForTest } from '../mocks/hover-toggle-controller'
5+
6+
import type { AgentMode } from '../../utils/constants'
7+
8+
describe('AgentModeToggle - buildExpandedSegments', () => {
9+
const modes: AgentMode[] = ['FAST', 'MAX', 'PLAN']
10+
11+
for (const mode of modes) {
12+
test(`returns segments with active indicator for ${mode}`, () => {
13+
const segs = buildExpandedSegments(mode)
14+
// 3 mode options + 1 active indicator
15+
expect(segs.length).toBe(4)
16+
17+
// Current mode is disabled among the choices
18+
const current = segs.find((s) => s.id === mode)
19+
expect(current?.disabled).toBe(true)
20+
21+
// Active indicator has expected id and flags
22+
const active = segs.find((s) => s.id === `active-${mode}`)
23+
expect(active).toBeTruthy()
24+
expect(active?.isSelected).toBe(true)
25+
expect(active?.defaultHighlighted).toBe(true)
26+
})
27+
}
28+
})
29+
30+
describe('AgentModeToggle - resolveAgentModeClick', () => {
31+
test('clicking active indicator returns closeActive', () => {
32+
const action = resolveAgentModeClick('FAST', 'active-FAST', true)
33+
expect(action).toEqual({ type: 'closeActive' })
34+
})
35+
36+
test('with onSelectMode provided, clicking different mode selects it', () => {
37+
const action = resolveAgentModeClick('FAST', 'MAX', true)
38+
expect(action).toEqual({ type: 'selectMode', mode: 'MAX' })
39+
})
40+
41+
test('without onSelectMode, clicking different mode toggles', () => {
42+
const action = resolveAgentModeClick('FAST', 'PLAN', false)
43+
expect(action).toEqual({ type: 'toggleMode', mode: 'PLAN' })
44+
})
45+
})
46+
47+
describe('useHoverToggle timing (controller)', () => {
48+
let originalSetTimeout: typeof setTimeout
49+
let originalClearTimeout: typeof clearTimeout
50+
let originalNow: typeof Date.now
51+
52+
let timers: { id: number; ms: number; fn: Function; active: boolean }[]
53+
let nextId: number
54+
55+
const runAll = () => {
56+
for (const t of timers) {
57+
if (t.active) t.fn()
58+
}
59+
timers = []
60+
}
61+
62+
beforeEach(() => {
63+
timers = []
64+
nextId = 1
65+
originalSetTimeout = setTimeout
66+
originalClearTimeout = clearTimeout
67+
originalNow = Date.now
68+
69+
let now = 1_000
70+
Date.now = () => now
71+
;(Date.now as any).set = (v: number) => {
72+
now = v
73+
}
74+
75+
globalThis.setTimeout = ((fn: Function, ms?: number) => {
76+
const id = nextId++
77+
timers.push({ id, ms: Number(ms ?? 0), fn, active: true })
78+
return id as any
79+
}) as any
80+
81+
globalThis.clearTimeout = ((id?: any) => {
82+
const rec = timers.find((t) => t.id === id)
83+
if (rec) rec.active = false
84+
}) as any
85+
})
86+
87+
afterEach(() => {
88+
globalThis.setTimeout = originalSetTimeout
89+
globalThis.clearTimeout = originalClearTimeout
90+
Date.now = originalNow
91+
})
92+
93+
test('scheduleOpen waits OPEN_DELAY_MS then opens', () => {
94+
const ctl = createHoverToggleControllerForTest()
95+
expect(ctl.isOpen).toBe(false)
96+
ctl.scheduleOpen()
97+
expect(timers.length).toBe(1)
98+
expect(timers[0].ms).toBe(OPEN_DELAY_MS)
99+
runAll()
100+
expect(ctl.isOpen).toBe(true)
101+
})
102+
103+
test('scheduleClose waits CLOSE_DELAY_MS then closes', () => {
104+
const ctl = createHoverToggleControllerForTest()
105+
ctl.openNow()
106+
expect(ctl.isOpen).toBe(true)
107+
ctl.scheduleClose()
108+
expect(timers.length).toBe(1)
109+
expect(timers[0].ms).toBe(CLOSE_DELAY_MS)
110+
runAll()
111+
expect(ctl.isOpen).toBe(false)
112+
})
113+
114+
test('closeNow(true) suppresses reopen until time passes', () => {
115+
const ctl = createHoverToggleControllerForTest()
116+
ctl.closeNow(true)
117+
ctl.scheduleOpen()
118+
expect(timers.length).toBe(0)
119+
;(Date.now as any).set(1_000 + REOPEN_SUPPRESS_MS + 1)
120+
ctl.scheduleOpen()
121+
expect(timers.length).toBe(1)
122+
expect(timers[0].ms).toBe(OPEN_DELAY_MS)
123+
})
124+
})
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, test, expect } from 'bun:test'
2+
import stringWidth from 'string-width'
3+
4+
import {
5+
type Segment,
6+
processSegments,
7+
type ProcessedSegment,
8+
} from '../../components/segmented-control'
9+
10+
import type { ChatTheme } from '../../types/theme-system'
11+
12+
const theme: ChatTheme = {
13+
// Core
14+
primary: '#ff0',
15+
secondary: '#888',
16+
success: '#0f0',
17+
error: '#f00',
18+
warning: '#fa0',
19+
info: '#0af',
20+
// Neutral
21+
foreground: '#fff',
22+
background: 'transparent',
23+
muted: '#777',
24+
border: '#333',
25+
surface: '#000',
26+
surfaceHover: '#222',
27+
// Context
28+
aiLine: '#0a0',
29+
userLine: '#08f',
30+
agentToggleHeaderBg: '#f60',
31+
agentToggleExpandedBg: '#14e',
32+
agentFocusedBg: '#334155',
33+
agentContentBg: '#000000',
34+
// Input
35+
inputBg: '#000',
36+
inputFg: '#fff',
37+
inputFocusedBg: '#000',
38+
inputFocusedFg: '#fff',
39+
// Modes
40+
modeFastBg: '#f60',
41+
modeFastText: '#f60',
42+
modeMaxBg: '#d22',
43+
modeMaxText: '#d22',
44+
modePlanBg: '#14e',
45+
modePlanText: '#14e',
46+
// Markdown
47+
markdown: {
48+
codeBackground: '#111',
49+
codeHeaderFg: '#555',
50+
inlineCodeFg: '#fff',
51+
codeTextFg: '#fff',
52+
headingFg: {
53+
1: '#ff0',
54+
2: '#ff0',
55+
3: '#ff0',
56+
4: '#ff0',
57+
5: '#ff0',
58+
6: '#ff0',
59+
},
60+
listBulletFg: '#aaa',
61+
blockquoteBorderFg: '#444',
62+
blockquoteTextFg: '#eee',
63+
dividerFg: '#333',
64+
codeMonochrome: true,
65+
},
66+
messageTextAttributes: 0,
67+
}
68+
69+
describe('SegmentedControl - processSegments', () => {
70+
test('computes width from label using string-width', () => {
71+
const segments: Segment[] = [
72+
{ id: 'FAST', label: 'FAST' },
73+
{ id: 'MAX', label: 'MAX' },
74+
{ id: 'PLAN', label: 'PLAN' },
75+
]
76+
77+
const processed = processSegments(segments, null, false, theme)
78+
const widths = processed.map((s) => s.width)
79+
expect(widths).toEqual([
80+
stringWidth(' FAST '),
81+
stringWidth(' MAX '),
82+
stringWidth(' PLAN '),
83+
])
84+
})
85+
86+
test('applies defaultHighlighted when nothing hovered', () => {
87+
const segments: Segment[] = [
88+
{ id: 'FAST', label: 'FAST' },
89+
{ id: 'MAX', label: 'MAX' },
90+
{
91+
id: 'active-FAST',
92+
label: '> FAST',
93+
isSelected: true,
94+
defaultHighlighted: true,
95+
},
96+
]
97+
98+
const processed = processSegments(segments, null, false, theme)
99+
const map: Record<string, ProcessedSegment> = Object.fromEntries(
100+
processed.map((p) => [p.id, p]),
101+
)
102+
103+
expect(map['active-FAST'].leftBorderColor).toBe(theme.foreground)
104+
expect(map['FAST'].leftBorderColor).toBe(theme.border)
105+
})
106+
107+
test('hovering a segment highlights it', () => {
108+
const segments: Segment[] = [
109+
{ id: 'FAST', label: 'FAST' },
110+
{ id: 'MAX', label: 'MAX' },
111+
{ id: 'PLAN', label: 'PLAN' },
112+
]
113+
const processed = processSegments(segments, 'MAX', true, theme)
114+
const map: Record<string, ProcessedSegment> = Object.fromEntries(
115+
processed.map((p) => [p.id, p]),
116+
)
117+
118+
expect(map.MAX.isHovered).toBe(true)
119+
expect(map.MAX.leftBorderColor).toBe(theme.foreground)
120+
expect(map.FAST.leftBorderColor).toBe(theme.border)
121+
})
122+
123+
test('disabled segments use muted text and border colors', () => {
124+
const segments: Segment[] = [
125+
{ id: 'FAST', label: 'FAST', disabled: true },
126+
{ id: 'MAX', label: 'MAX' },
127+
]
128+
const [fast, max] = processSegments(segments, null, false, theme)
129+
expect(fast.textColor).toBe(theme.muted)
130+
expect(fast.leftBorderColor).toBe(theme.border)
131+
expect(max.textColor).toBe(theme.foreground)
132+
})
133+
})
134+
135+
describe('SegmentedControl - string width with double-width glyphs', () => {
136+
test('emoji and CJK characters increase width correctly', () => {
137+
const segments: Segment[] = [
138+
{ id: 'E', label: '🚀MAX' },
139+
{ id: 'CJK', label: '计划' },
140+
]
141+
142+
const processed = processSegments(segments, null, false, theme)
143+
144+
expect(processed[0].width).toBe(stringWidth(' 🚀MAX '))
145+
expect(processed[1].width).toBe(stringWidth(' 计划 '))
146+
})
147+
})

0 commit comments

Comments
 (0)