Skip to content

Commit c41d64d

Browse files
committed
doc: Document Finch compose usage; improve delete confirms and long-message UX
1 parent 6f0754a commit c41d64d

File tree

4 files changed

+138
-25
lines changed

4 files changed

+138
-25
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,40 @@ curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/d
3535
| HOST_PORT=8788 docker compose -f - up -d --pull always
3636
```
3737

38+
Finch equivalent (download first):
39+
40+
```
41+
curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml -o /tmp/treechat.pull.yml
42+
HOST_PORT=8787 finch compose -f /tmp/treechat.pull.yml up -d
43+
```
44+
45+
To shut it down:
46+
47+
```
48+
curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml -o /tmp/treechat.pull.yml
49+
docker compose -f /tmp/treechat.pull.yml down
50+
finch compose -f /tmp/treechat.pull.yml down
51+
```
52+
3853
To run with real model calls, pass `USE_MOCK=0` and an API key to `docker compose`:
3954

4055
```
4156
curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml \
4257
| USE_MOCK=0 OPENROUTER_API_KEY=... docker compose -f - up -d --pull always
4358
```
4459

60+
Finch equivalent:
61+
62+
```
63+
USE_MOCK=0 OPENROUTER_API_KEY=... finch compose -f /tmp/treechat.pull.yml up -d
64+
```
65+
4566
### Option B: Local (build from source)
4667

4768
From this repo:
4869

4970
- `docker compose up --build`
71+
- `finch compose up --build` (Finch)
5072
- open http://localhost:8787
5173

5274
See `docs/DOCKER.md`.

client/src/components/LeftSidebar.tsx

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useRef, useState } from 'react'
22
import { useNavigate, useParams } from 'react-router-dom'
3-
import { Check, Cog, PanelLeftClose, PenLine, SlidersHorizontal, SquarePen, Trash2, X } from 'lucide-react'
3+
import { Check, Cog, PanelLeftClose, PenLine, Bot, SquarePen, Trash2, X } from 'lucide-react'
44
import { ConversationListItem, listConversations, deleteConversation, getConversationSummary, updateConversationSummary } from '../lib/api'
55

66
export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () => void, onOpenSettings: () => void }) {
@@ -18,6 +18,29 @@ export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () =
1818
const noticeTimer = useRef<number | null>(null)
1919
const editRef = useRef<HTMLTextAreaElement | null>(null)
2020

21+
const cancelConfirm = () => {
22+
setConfirmId(null)
23+
}
24+
25+
const confirmDelete = async () => {
26+
if (!confirmId) return
27+
if (deleting) return
28+
const toDelete = confirmId as string
29+
setDeleting(true)
30+
try {
31+
await deleteConversation(toDelete)
32+
// Refresh list from server to ensure DB state is reflected
33+
const list = await listConversations()
34+
setItems(list)
35+
if (id === toDelete) navigate('/')
36+
setConfirmId(null)
37+
} catch (e: any) {
38+
setToast(`Delete failed: ${e?.message || 'server error'}`)
39+
} finally {
40+
setDeleting(false)
41+
}
42+
}
43+
2144
const commitEdit = async (conversationId: string) => {
2245
if (saving) return
2346
const length = [...editingValue].length
@@ -58,6 +81,25 @@ export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () =
5881
return () => { alive = false }
5982
}, [id])
6083

84+
useEffect(() => {
85+
if (!confirmId) return
86+
const onKeyDown = (e: KeyboardEvent) => {
87+
if (!confirmId) return
88+
if (e.metaKey || e.ctrlKey || e.altKey) return
89+
if (e.key === 'Enter') {
90+
e.preventDefault()
91+
e.stopPropagation()
92+
void confirmDelete()
93+
} else if (e.key === 'Backspace' || e.key === 'Delete') {
94+
e.preventDefault()
95+
e.stopPropagation()
96+
cancelConfirm()
97+
}
98+
}
99+
window.addEventListener('keydown', onKeyDown, { capture: true })
100+
return () => { window.removeEventListener('keydown', onKeyDown, { capture: true }) }
101+
}, [confirmId, deleting, id, navigate])
102+
61103
return (
62104
<div className="left-sidebar">
63105
<div style={{ marginBottom: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
@@ -225,7 +267,7 @@ export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () =
225267
aria-label="Open models"
226268
title="Open models"
227269
>
228-
<SlidersHorizontal size={16} />
270+
<Bot size={16} />
229271
</button>
230272
<button
231273
className="icon-button"
@@ -237,7 +279,7 @@ export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () =
237279
</button>
238280
</div>
239281
{confirmId && (
240-
<div className="modal-overlay" onClick={() => setConfirmId(null)}>
282+
<div className="modal-overlay" onClick={cancelConfirm}>
241283
<div className="modal" onClick={e => e.stopPropagation()}>
242284
<div className="modal-body">
243285
Delete the entire conversation?
@@ -246,26 +288,11 @@ export default function LeftSidebar({ onClose, onOpenSettings }: { onClose: () =
246288
</div>
247289
</div>
248290
<div className="modal-actions">
249-
<button className="button" onClick={() => setConfirmId(null)} disabled={deleting}>Cancel</button>
291+
<button className="button" onClick={cancelConfirm}>Cancel</button>
250292
<button
251293
className="button danger"
252294
disabled={deleting}
253-
onClick={async () => {
254-
const toDelete = confirmId as string
255-
setDeleting(true)
256-
try {
257-
await deleteConversation(toDelete)
258-
// Refresh list from server to ensure DB state is reflected
259-
const list = await listConversations()
260-
setItems(list)
261-
if (id === toDelete) navigate('/')
262-
setConfirmId(null)
263-
} catch (e: any) {
264-
setToast(`Delete failed: ${e?.message || 'server error'}`)
265-
} finally {
266-
setDeleting(false)
267-
}
268-
}}
295+
onClick={confirmDelete}
269296
>
270297
{deleting ? 'Deleting…' : 'Delete'}
271298
</button>

client/src/components/MessageNode.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
2-
import { Pencil, RefreshCw, Trash2 } from 'lucide-react'
2+
import { Maximize2, Minimize2, Pencil, RefreshCw, Trash2 } from 'lucide-react'
33
import ReactMarkdown from 'react-markdown'
44
import { ConversationState, MessageNode as TMessageNode } from '../types'
55
import Composer from './Composer'
@@ -29,6 +29,10 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
2929
const [confirming, setConfirming] = useState(false)
3030
const [editModelOpen, setEditModelOpen] = useState(false)
3131
const editMenuRef = useRef<HTMLDivElement | null>(null)
32+
const [userExpanded, setUserExpanded] = useState(false)
33+
34+
const cancelDelete = () => setConfirming(false)
35+
const confirmDelete = () => { setConfirming(false); onDelete(node.id) }
3236

3337
const [retryMenuOpen, setRetryMenuOpen] = useState(false)
3438
const [retryMenuPane, setRetryMenuPane] = useState<'root' | 'models'>('root')
@@ -57,6 +61,25 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
5761
document.addEventListener('click', onDocClick)
5862
return () => { document.removeEventListener('click', onDocClick) }
5963
}, [retryMenuOpen])
64+
65+
useEffect(() => {
66+
if (!confirming) return
67+
const onKeyDown = (e: KeyboardEvent) => {
68+
if (!confirming) return
69+
if (e.metaKey || e.ctrlKey || e.altKey) return
70+
if (e.key === 'Enter') {
71+
e.preventDefault()
72+
e.stopPropagation()
73+
confirmDelete()
74+
} else if (e.key === 'Backspace' || e.key === 'Delete') {
75+
e.preventDefault()
76+
e.stopPropagation()
77+
cancelDelete()
78+
}
79+
}
80+
window.addEventListener('keydown', onKeyDown, { capture: true })
81+
return () => { window.removeEventListener('keydown', onKeyDown, { capture: true }) }
82+
}, [confirming, node.id])
6083
useLayoutEffect(() => {
6184
if (!retryMenuOpen) {
6285
retryMenuShiftXRef.current = 0
@@ -102,6 +125,23 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
102125
const roleClass = node.role === 'user' ? 'node-user' : node.role === 'assistant' ? 'node-assistant' : 'node-system'
103126
const isActive = state.selectedLeafId === node.id
104127
const roleLabel = node.role.charAt(0).toUpperCase() + node.role.slice(1).toLowerCase()
128+
const isLongUserContent = useMemo(() => {
129+
if (node.role !== 'user') return false
130+
if (editing) return false
131+
if (!node.content) return false
132+
const content = node.content
133+
const lines = content.split('\n')
134+
const lineCount = lines.length
135+
const longestLine = lines.reduce((m, l) => Math.max(m, l.length), 0)
136+
return content.length >= 800 || lineCount >= 14 || longestLine >= 240
137+
}, [editing, node.content, node.role])
138+
useEffect(() => {
139+
if (!isLongUserContent && userExpanded) setUserExpanded(false)
140+
}, [isLongUserContent, userExpanded])
141+
useEffect(() => {
142+
if (node.role !== 'user') return
143+
setUserExpanded(false)
144+
}, [node.content, node.id, node.role])
105145

106146
return (
107147
<div>
@@ -150,7 +190,7 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
150190
)}
151191
</div>
152192
) : (
153-
<div className="node-content plain">{node.content || <span style={{ color: 'var(--muted-2)' }}>GENERATING...</span>}</div>
193+
<div className={`node-content plain ${isLongUserContent ? (userExpanded ? 'long expanded' : 'long') : ''}`}>{node.content || <span style={{ color: 'var(--muted-2)' }}>GENERATING...</span>}</div>
154194
)}
155195
</>
156196
)}
@@ -219,6 +259,16 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
219259
)}
220260
{node.role === 'user' && (
221261
<>
262+
{isLongUserContent && (
263+
<button
264+
className="icon-button"
265+
aria-label={userExpanded ? 'Collapse long message' : 'Expand long message'}
266+
title={userExpanded ? 'Collapse' : 'Expand'}
267+
onClick={() => setUserExpanded(e => !e)}
268+
>
269+
{userExpanded ? <Minimize2 size={16} strokeWidth={2} /> : <Maximize2 size={16} strokeWidth={2} />}
270+
</button>
271+
)}
222272
<button
223273
className="icon-button"
224274
aria-label="Edit message"
@@ -278,14 +328,14 @@ export default function MessageNode({ node, state, onSelect, onRetry, onRetryAll
278328
)}
279329

280330
{confirming && (
281-
<div className="modal-overlay" onClick={() => setConfirming(false)}>
331+
<div className="modal-overlay" onClick={cancelDelete}>
282332
<div className="modal" onClick={e => e.stopPropagation()}>
283333
<div className="modal-body">
284334
Deleting a message deletes EVERY child messages in EVERY branch. Confirmation to delete?
285335
</div>
286336
<div className="modal-actions">
287-
<button className="button pale" onClick={() => setConfirming(false)}>No</button>
288-
<button className="button danger" onClick={() => { setConfirming(false); onDelete(node.id) }}>Yes</button>
337+
<button className="button pale" onClick={cancelDelete}>No</button>
338+
<button className="button danger" onClick={confirmDelete}>Yes</button>
289339
</div>
290340
</div>
291341
</div>

docs/DOCKER.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Bundling Postgres into the same container is possible but not recommended (harde
1212
From the repo root:
1313

1414
- `docker compose up --build`
15+
- `finch compose up --build` (Finch)
1516

1617
Then open:
1718

@@ -30,6 +31,7 @@ To run with real model calls, set `USE_MOCK=0` and pass an API key:
3031
If port 8787 is busy, pick another host port:
3132

3233
- `HOST_PORT=8788 docker compose up --build`
34+
- `HOST_PORT=8788 finch compose up --build` (Finch)
3335

3436
## “One command” run (pull prebuilt image)
3537

@@ -39,10 +41,22 @@ This repo includes `docker-compose.pull.yml` which is intended to be fetched and
3941

4042
- `curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml | docker compose -f - up -d --pull always`
4143

44+
Finch equivalent (download first):
45+
46+
- `curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml -o /tmp/treechat.pull.yml`
47+
- `finch compose -f /tmp/treechat.pull.yml up -d`
48+
4249
After it finishes, open:
4350

4451
- http://localhost:8787
4552

4653
To enable real model calls, set `USE_MOCK=0` and pass either `OPENROUTER_API_KEY` or `OPENAI_API_KEY` to the `docker compose` command:
4754

4855
- `curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml | USE_MOCK=0 OPENROUTER_API_KEY=... docker compose -f - up -d --pull always`
56+
- `USE_MOCK=0 OPENROUTER_API_KEY=... finch compose -f /tmp/treechat.pull.yml up -d` (Finch)
57+
58+
To shut down (Docker or Finch), run `down` with the same compose file:
59+
60+
- `curl -fsSL https://raw.githubusercontent.com/yxzwayne/treechat/refs/heads/main/docker-compose.pull.yml -o /tmp/treechat.pull.yml`
61+
- `docker compose -f /tmp/treechat.pull.yml down`
62+
- `finch compose -f /tmp/treechat.pull.yml down`

0 commit comments

Comments
 (0)