Skip to content

Commit a2fbf3c

Browse files
committed
add quadratic scrolling
1 parent b7bf134 commit a2fbf3c

File tree

5 files changed

+412
-14
lines changed

5 files changed

+412
-14
lines changed

cli/src/chat.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { useTheme, useResolvedThemeName } from './hooks/use-theme'
3535
import { useChatStore } from './state/chat-store'
3636
import { flushAnalytics } from './utils/analytics'
3737
import { getUserCredentials } from './utils/auth'
38-
import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
38+
import { QuadraticScrollAccel } from './utils/chat-scroll-accel'
3939
import { createValidationErrorBlocks } from './utils/create-validation-error-blocks'
4040
import { formatQueuedPreview } from './utils/helpers'
4141
import {
@@ -498,7 +498,8 @@ export const App = ({
498498
useChatScrollbox(scrollRef, messages, agentRefsMap)
499499

500500
const inertialScrollAcceleration = useMemo(
501-
() => createChatScrollAcceleration(),
501+
() => new QuadraticScrollAccel(),
502+
// () => createChatScrollAcceleration(),
502503
[],
503504
)
504505

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import { describe, test, expect, beforeEach } from 'bun:test'
2+
3+
import { Queue } from '../arrays'
4+
5+
describe('Queue', () => {
6+
describe('constructor', () => {
7+
test('creates empty queue', () => {
8+
const q = new Queue<number>()
9+
expect(q.length).toBe(0)
10+
expect(q.peek()).toBeUndefined()
11+
})
12+
13+
test('creates queue from array', () => {
14+
const q = new Queue([1, 2, 3])
15+
expect(q.length).toBe(3)
16+
expect(q.peek()).toBe(1)
17+
})
18+
19+
test('allocates extra capacity', () => {
20+
const q = new Queue([1, 2, 3])
21+
// Should have allocated double capacity (3 items + 3 extra)
22+
q.enqueue(4, 5, 6)
23+
expect(q.length).toBe(6)
24+
})
25+
})
26+
27+
describe('Queue.from', () => {
28+
test('creates queue from iterable', () => {
29+
const q = Queue.from([1, 2, 3])
30+
expect(q.length).toBe(3)
31+
expect(q.dequeue()).toBe(1)
32+
expect(q.dequeue()).toBe(2)
33+
expect(q.dequeue()).toBe(3)
34+
})
35+
36+
test('creates queue from Set', () => {
37+
const q = Queue.from(new Set([1, 2, 3]))
38+
expect(q.length).toBe(3)
39+
})
40+
41+
test('creates empty queue from empty iterable', () => {
42+
const q = Queue.from([])
43+
expect(q.length).toBe(0)
44+
})
45+
})
46+
47+
describe('enqueue', () => {
48+
test('adds single item', () => {
49+
const q = new Queue<number>()
50+
q.enqueue(1)
51+
expect(q.length).toBe(1)
52+
expect(q.peek()).toBe(1)
53+
})
54+
55+
test('adds multiple items', () => {
56+
const q = new Queue<number>()
57+
q.enqueue(1, 2, 3)
58+
expect(q.length).toBe(3)
59+
expect(q.dequeue()).toBe(1)
60+
expect(q.dequeue()).toBe(2)
61+
expect(q.dequeue()).toBe(3)
62+
})
63+
64+
test('grows capacity when needed', () => {
65+
const q = new Queue([1])
66+
// Initial capacity is 2 (1 item + 1 extra)
67+
q.enqueue(2, 3, 4)
68+
expect(q.length).toBe(4)
69+
expect(q.dequeue()).toBe(1)
70+
expect(q.dequeue()).toBe(2)
71+
expect(q.dequeue()).toBe(3)
72+
expect(q.dequeue()).toBe(4)
73+
})
74+
75+
test('handles wrap-around correctly', () => {
76+
const q = new Queue([1, 2, 3])
77+
q.dequeue() // Remove 1, head moves to index 1
78+
q.dequeue() // Remove 2, head moves to index 2
79+
q.enqueue(4, 5) // Should wrap around
80+
expect(q.length).toBe(3)
81+
expect(q.dequeue()).toBe(3)
82+
expect(q.dequeue()).toBe(4)
83+
expect(q.dequeue()).toBe(5)
84+
})
85+
})
86+
87+
describe('dequeue', () => {
88+
test('removes and returns first item', () => {
89+
const q = new Queue([1, 2, 3])
90+
expect(q.dequeue()).toBe(1)
91+
expect(q.length).toBe(2)
92+
expect(q.peek()).toBe(2)
93+
})
94+
95+
test('returns undefined when empty', () => {
96+
const q = new Queue<number>()
97+
expect(q.dequeue()).toBeUndefined()
98+
})
99+
100+
test('maintains FIFO order', () => {
101+
const q = new Queue<number>()
102+
q.enqueue(1, 2, 3, 4, 5)
103+
expect(q.dequeue()).toBe(1)
104+
expect(q.dequeue()).toBe(2)
105+
expect(q.dequeue()).toBe(3)
106+
expect(q.dequeue()).toBe(4)
107+
expect(q.dequeue()).toBe(5)
108+
expect(q.dequeue()).toBeUndefined()
109+
})
110+
111+
test('wraps head pointer correctly', () => {
112+
const q = new Queue([1, 2])
113+
q.dequeue()
114+
q.dequeue()
115+
q.enqueue(3, 4)
116+
expect(q.dequeue()).toBe(3)
117+
expect(q.dequeue()).toBe(4)
118+
})
119+
})
120+
121+
describe('peek', () => {
122+
test('returns first item without removing', () => {
123+
const q = new Queue([1, 2, 3])
124+
expect(q.peek()).toBe(1)
125+
expect(q.length).toBe(3)
126+
expect(q.peek()).toBe(1)
127+
})
128+
129+
test('returns undefined when empty', () => {
130+
const q = new Queue<number>()
131+
expect(q.peek()).toBeUndefined()
132+
})
133+
134+
test('tracks changes after dequeue', () => {
135+
const q = new Queue([1, 2, 3])
136+
q.dequeue()
137+
expect(q.peek()).toBe(2)
138+
q.dequeue()
139+
expect(q.peek()).toBe(3)
140+
})
141+
})
142+
143+
describe('at', () => {
144+
let q: Queue<number>
145+
146+
beforeEach(() => {
147+
q = new Queue([1, 2, 3, 4, 5])
148+
})
149+
150+
test('returns element at positive index', () => {
151+
expect(q.at(0)).toBe(1)
152+
expect(q.at(1)).toBe(2)
153+
expect(q.at(2)).toBe(3)
154+
expect(q.at(4)).toBe(5)
155+
})
156+
157+
test('returns element at negative index', () => {
158+
expect(q.at(-1)).toBe(5)
159+
expect(q.at(-2)).toBe(4)
160+
expect(q.at(-5)).toBe(1)
161+
})
162+
163+
test('returns undefined for out of bounds index', () => {
164+
expect(q.at(5)).toBeUndefined()
165+
expect(q.at(100)).toBeUndefined()
166+
expect(q.at(-6)).toBeUndefined()
167+
expect(q.at(-100)).toBeUndefined()
168+
})
169+
170+
test('handles wrap-around correctly', () => {
171+
q.dequeue() // Remove 1
172+
q.dequeue() // Remove 2
173+
q.enqueue(6, 7) // Add to wrapped positions
174+
expect(q.at(0)).toBe(3)
175+
expect(q.at(1)).toBe(4)
176+
expect(q.at(2)).toBe(5)
177+
expect(q.at(3)).toBe(6)
178+
expect(q.at(4)).toBe(7)
179+
expect(q.at(-1)).toBe(7)
180+
})
181+
182+
test('returns undefined for empty queue', () => {
183+
const emptyQ = new Queue<number>()
184+
expect(emptyQ.at(0)).toBeUndefined()
185+
expect(emptyQ.at(-1)).toBeUndefined()
186+
})
187+
})
188+
189+
describe('length', () => {
190+
test('tracks queue size', () => {
191+
const q = new Queue<number>()
192+
expect(q.length).toBe(0)
193+
q.enqueue(1)
194+
expect(q.length).toBe(1)
195+
q.enqueue(2, 3)
196+
expect(q.length).toBe(3)
197+
q.dequeue()
198+
expect(q.length).toBe(2)
199+
q.dequeue()
200+
q.dequeue()
201+
expect(q.length).toBe(0)
202+
})
203+
204+
test('maintains correct length through wrap-around', () => {
205+
const q = new Queue([1, 2])
206+
q.dequeue()
207+
q.enqueue(3)
208+
expect(q.length).toBe(2)
209+
q.dequeue()
210+
expect(q.length).toBe(1)
211+
})
212+
})
213+
214+
describe('edge cases', () => {
215+
test('handles alternating enqueue/dequeue', () => {
216+
const q = new Queue<number>()
217+
q.enqueue(1)
218+
expect(q.dequeue()).toBe(1)
219+
q.enqueue(2)
220+
expect(q.dequeue()).toBe(2)
221+
q.enqueue(3)
222+
expect(q.dequeue()).toBe(3)
223+
})
224+
225+
test('handles different data types', () => {
226+
const stringQ = new Queue(['a', 'b', 'c'])
227+
expect(stringQ.dequeue()).toBe('a')
228+
229+
const objectQ = new Queue([{ id: 1 }, { id: 2 }])
230+
expect(objectQ.dequeue()).toEqual({ id: 1 })
231+
232+
const mixedQ = new Queue<string | number>([1, 'two', 3])
233+
expect(mixedQ.dequeue()).toBe(1)
234+
expect(mixedQ.dequeue()).toBe('two')
235+
})
236+
237+
test('maintains integrity after capacity expansion', () => {
238+
const q = new Queue([1, 2])
239+
// Trigger expansion
240+
q.enqueue(3, 4, 5, 6, 7, 8)
241+
expect(q.length).toBe(8)
242+
for (let i = 1; i <= 8; i++) {
243+
expect(q.dequeue()).toBe(i)
244+
}
245+
})
246+
247+
test('handles large number of operations', () => {
248+
const q = new Queue<number>()
249+
const iterations = 1000
250+
251+
// Enqueue many items
252+
for (let i = 0; i < iterations; i++) {
253+
q.enqueue(i)
254+
}
255+
expect(q.length).toBe(iterations)
256+
257+
// Dequeue all items
258+
for (let i = 0; i < iterations; i++) {
259+
expect(q.dequeue()).toBe(i)
260+
}
261+
expect(q.length).toBe(0)
262+
})
263+
})
264+
})

cli/src/utils/arrays.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,99 @@ export function* range(start: number, stop?: number, step: number = 1) {
1919
}
2020
}
2121
}
22+
23+
export function repeated<T>(value: T, count: number): T[] {
24+
return Array.from({ length: count }, () => value)
25+
}
26+
27+
export class Queue<T> {
28+
private _items: T[]
29+
private _head: number
30+
private _length: number
31+
private _defaultCapacity: number
32+
33+
constructor(items: T[] | undefined = undefined, capacity: number = 100) {
34+
this._defaultCapacity = capacity
35+
if (!items) {
36+
items = []
37+
}
38+
this._items = items.length
39+
? [
40+
...items,
41+
...repeated(undefined as T, Math.max(0, capacity - items.length)),
42+
]
43+
: [undefined as T]
44+
this._head = 0
45+
this._length = items.length
46+
}
47+
48+
static from<T>(iterable: Iterable<T>): Queue<T> {
49+
return new Queue<T>([...iterable])
50+
}
51+
52+
enqueue(...items: T[]) {
53+
if (this._items.length < this._length + items.length) {
54+
const newItems = [
55+
...repeated(undefined as T, this._length),
56+
...items,
57+
...repeated(undefined as T, this._length),
58+
]
59+
for (let i = 0; i < this._length; i++) {
60+
newItems[i] = this.at(i)!
61+
}
62+
63+
this._items = newItems
64+
this._head = 0
65+
this._length += items.length
66+
return
67+
}
68+
69+
let index = (this._head + this._length) % this._items.length
70+
for (const item of items) {
71+
this._items[index] = item
72+
index = (index + 1) % this._items.length
73+
}
74+
this._length += items.length
75+
}
76+
77+
dequeue(): T | undefined {
78+
if (this._length === 0) {
79+
return undefined
80+
}
81+
82+
const item = this._items[this._head]
83+
this._items[this._head] = undefined as T
84+
this._head = (this._head + 1) % this._items.length
85+
this._length--
86+
return item
87+
}
88+
89+
peek(): T | undefined {
90+
if (this._length === 0) {
91+
return undefined
92+
}
93+
return this._items[this._head]
94+
}
95+
96+
at(index: number): T | undefined {
97+
if (index >= this._length || index < -this.length) {
98+
return undefined
99+
}
100+
if (index < 0) {
101+
return this._items[
102+
(this._head + this._length + index) % this._items.length
103+
]
104+
}
105+
return this._items[(this._head + index) % this._items.length]
106+
}
107+
108+
clear(): void {
109+
this._items = repeated(undefined as T, this._defaultCapacity)
110+
this._head = 0
111+
this._length = 0
112+
}
113+
114+
get length() {
115+
return this._length
116+
}
117+
}

0 commit comments

Comments
 (0)