@polderlabs/bizar 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/src/server/opencode-sdk.mjs +72 -0
- package/bizar-dash/src/server/routes/background.mjs +92 -0
- package/bizar-dash/src/server/routes/chat.mjs +300 -123
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +26 -0
- package/bizar-dash/src/server/routes/tasks.mjs +59 -1
- package/bizar-dash/src/server/task-delegator.mjs +154 -8
- package/bizar-dash/src/server/tasks-store.mjs +50 -2
- package/bizar-dash/src/web/components/background/AttachButton.tsx +96 -0
- package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +122 -0
- package/bizar-dash/src/web/components/chat/AgentNode.tsx +127 -0
- package/bizar-dash/src/web/components/chat/AgentTree.tsx +80 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +76 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +144 -0
- package/bizar-dash/src/web/components/chat/ChatRail.tsx +387 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +28 -7
- package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +58 -0
- package/bizar-dash/src/web/components/chat/MessageBlock.tsx +260 -0
- package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +206 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +33 -5
- package/bizar-dash/src/web/components/chat/_legacy.ts +30 -0
- package/bizar-dash/src/web/components/chat/index.ts +11 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +345 -167
- package/bizar-dash/src/web/components/tasks/BacklogPanel.css +109 -0
- package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +209 -0
- package/bizar-dash/src/web/styles/chat.css +1536 -133
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +3 -0
- package/bizar-dash/src/web/views/Chat.tsx +147 -57
- package/bizar-dash/src/web/views/Tasks.tsx +23 -1
- package/cli/bg.mjs +94 -71
- package/cli/bin.mjs +36 -8
- package/cli/service-controller.mjs +587 -0
- package/cli/service-controller.test.mjs +92 -0
- package/cli/service.mjs +162 -14
- package/config/agents/baldr.md +2 -0
- package/config/agents/browser-harness.md +2 -0
- package/config/agents/forseti.md +2 -0
- package/config/agents/frigg.md +2 -0
- package/config/agents/heimdall.md +2 -0
- package/config/agents/hermod.md +2 -0
- package/config/agents/mimir.md +2 -0
- package/config/agents/odin.md +2 -0
- package/config/agents/quick.md +2 -0
- package/config/agents/semble-search.md +2 -0
- package/config/agents/thor.md +2 -0
- package/config/agents/tyr.md +2 -0
- package/config/agents/vidarr.md +2 -0
- package/config/agents/vor.md +2 -0
- package/config/opencode.json.template +1 -0
- package/install.sh +448 -787
- package/package.json +2 -2
- package/packages/sdk/package.json +20 -0
- package/packages/sdk/src/client.ts +5 -0
- package/packages/sdk/src/errors.ts +11 -2
- package/packages/sdk/src/index.ts +19 -0
- package/packages/sdk/src/opencode-events.ts +134 -0
- package/packages/sdk/src/opencode-types.ts +66 -0
- package/packages/sdk/src/opencode.ts +335 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/components/chat/JumpToLatest.tsx — floating pill above the composer.
|
|
2
|
+
//
|
|
3
|
+
// Two visual variants:
|
|
4
|
+
// - streaming (current session is being replied to): pulsing indigo
|
|
5
|
+
// border + 3-dot bounce + green "live" badge
|
|
6
|
+
// - idle: neutral pill + down-arrow icon + "N new" badge
|
|
7
|
+
//
|
|
8
|
+
// Hidden entirely when the user is already at the bottom of the thread
|
|
9
|
+
// (controlled by the parent's `stickToBottom` flag).
|
|
10
|
+
|
|
11
|
+
import { ArrowDown } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
streaming: boolean;
|
|
15
|
+
newMessageCount: number;
|
|
16
|
+
onClick: () => void;
|
|
17
|
+
/** Optional label override (e.g. "Odin is replying"). */
|
|
18
|
+
streamingLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function JumpToLatest({
|
|
22
|
+
streaming,
|
|
23
|
+
newMessageCount,
|
|
24
|
+
onClick,
|
|
25
|
+
streamingLabel = 'Odin is replying',
|
|
26
|
+
}: Props) {
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
className={`jump-to-latest${streaming ? ' is-streaming' : ''}`}
|
|
31
|
+
onClick={onClick}
|
|
32
|
+
aria-label="Jump to latest message"
|
|
33
|
+
title="Jump to latest (⌘/Ctrl+End)"
|
|
34
|
+
>
|
|
35
|
+
{streaming ? (
|
|
36
|
+
<>
|
|
37
|
+
<span className="jump-dots" aria-hidden>
|
|
38
|
+
<span />
|
|
39
|
+
<span />
|
|
40
|
+
<span />
|
|
41
|
+
</span>
|
|
42
|
+
<span>{streamingLabel}</span>
|
|
43
|
+
{newMessageCount > 0 && (
|
|
44
|
+
<span className="jump-badge jump-badge-live">live</span>
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<ArrowDown size={14} aria-hidden />
|
|
50
|
+
<span>Jump to latest</span>
|
|
51
|
+
{newMessageCount > 0 && (
|
|
52
|
+
<span className="jump-badge">{newMessageCount} new</span>
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/components/chat/MessageBlock.tsx — single chat bubble with hover actions
|
|
2
|
+
// + phased halo animation while streaming.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors the design HTML's MessageBlock component (lines 2266–2390 of
|
|
5
|
+
// the source design). Each message has:
|
|
6
|
+
// - avatar (initial letter + halo pulse for phase think/stream/commit)
|
|
7
|
+
// - meta row (author + phase indicator + timestamp)
|
|
8
|
+
// - bubble (user or assistant styling)
|
|
9
|
+
// - hover action bar (copy / edit / regenerate / speak / thumbs-up / thumbs-down)
|
|
10
|
+
//
|
|
11
|
+
// Streaming flow (when `streaming: true` on the message):
|
|
12
|
+
// think (0–900ms) → dots, halo pulse phase=think
|
|
13
|
+
// stream (900–5400ms) → cursor blinks, halo pulse phase=stream
|
|
14
|
+
// commit (5400–5800ms) → green halo pulse, bubble flashes ring
|
|
15
|
+
// idle (5800ms+) → all animations stop, action bar fully visible
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
Copy,
|
|
20
|
+
Edit,
|
|
21
|
+
RefreshCw,
|
|
22
|
+
Volume2,
|
|
23
|
+
ThumbsUp,
|
|
24
|
+
ThumbsDown,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { StreamingIndicator } from './StreamingIndicator';
|
|
27
|
+
|
|
28
|
+
export type StreamingPhase = 'idle' | 'think' | 'stream' | 'commit';
|
|
29
|
+
|
|
30
|
+
export type MessageRole = 'user' | 'assistant' | 'system';
|
|
31
|
+
|
|
32
|
+
export type MessageContentBlock =
|
|
33
|
+
| { type: 'p'; text: string }
|
|
34
|
+
| { type: 'ul'; items: string[] }
|
|
35
|
+
| { type: 'code'; code: string }
|
|
36
|
+
| { type: 'text'; text: string };
|
|
37
|
+
|
|
38
|
+
export interface MessageBlockProps {
|
|
39
|
+
id?: string;
|
|
40
|
+
role: MessageRole;
|
|
41
|
+
content: MessageContentBlock[] | string;
|
|
42
|
+
agent?: string;
|
|
43
|
+
ts?: string;
|
|
44
|
+
streaming?: boolean;
|
|
45
|
+
feedback?: 'up' | 'down' | null;
|
|
46
|
+
onCopy?: (text: string) => void;
|
|
47
|
+
onEdit?: (msg: MessageBlockProps) => void;
|
|
48
|
+
onRegenerate?: (msg: MessageBlockProps) => void;
|
|
49
|
+
onSpeak?: (msg: MessageBlockProps) => void;
|
|
50
|
+
onFeedback?: (msg: MessageBlockProps, value: 'up' | 'down' | null) => void;
|
|
51
|
+
onReady?: (msg: MessageBlockProps) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getInitial(agent?: string): string {
|
|
55
|
+
if (!agent) return 'A';
|
|
56
|
+
return agent.replace(/^@/, '').charAt(0).toUpperCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function flattenText(content: MessageContentBlock[] | string): string {
|
|
60
|
+
if (typeof content === 'string') return content;
|
|
61
|
+
return content
|
|
62
|
+
.map((b) =>
|
|
63
|
+
b.type === 'p' || b.type === 'text'
|
|
64
|
+
? b.text
|
|
65
|
+
: b.type === 'ul'
|
|
66
|
+
? (b.items ?? []).join('\n')
|
|
67
|
+
: b.type === 'code'
|
|
68
|
+
? b.code
|
|
69
|
+
: '',
|
|
70
|
+
)
|
|
71
|
+
.join('\n\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function MessageBlock(props: MessageBlockProps) {
|
|
75
|
+
const {
|
|
76
|
+
role,
|
|
77
|
+
content,
|
|
78
|
+
agent,
|
|
79
|
+
ts,
|
|
80
|
+
streaming = false,
|
|
81
|
+
feedback: feedbackProp = null,
|
|
82
|
+
onCopy,
|
|
83
|
+
onEdit,
|
|
84
|
+
onRegenerate,
|
|
85
|
+
onSpeak,
|
|
86
|
+
onFeedback,
|
|
87
|
+
onReady,
|
|
88
|
+
} = props;
|
|
89
|
+
|
|
90
|
+
const [copied, setCopied] = useState(false);
|
|
91
|
+
const [feedback, setFeedback] = useState<'up' | 'down' | null>(feedbackProp);
|
|
92
|
+
const [phase, setPhase] = useState<StreamingPhase>(streaming ? 'think' : 'idle');
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!streaming) {
|
|
96
|
+
setPhase('idle');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const timers = [
|
|
100
|
+
window.setTimeout(() => setPhase('stream'), 900),
|
|
101
|
+
window.setTimeout(() => setPhase('commit'), 5400),
|
|
102
|
+
window.setTimeout(() => {
|
|
103
|
+
setPhase('idle');
|
|
104
|
+
onReady?.(props);
|
|
105
|
+
}, 5800),
|
|
106
|
+
];
|
|
107
|
+
return () => timers.forEach(window.clearTimeout);
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
}, [streaming]);
|
|
110
|
+
|
|
111
|
+
const handleCopy = () => {
|
|
112
|
+
const text = flattenText(content);
|
|
113
|
+
onCopy?.(text);
|
|
114
|
+
setCopied(true);
|
|
115
|
+
window.setTimeout(() => setCopied(false), 1400);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleFeedback = (kind: 'up' | 'down') => {
|
|
119
|
+
const next = feedback === kind ? null : kind;
|
|
120
|
+
setFeedback(next);
|
|
121
|
+
onFeedback?.(props, next);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (role === 'system') {
|
|
125
|
+
return (
|
|
126
|
+
<div className="msg msg-system">
|
|
127
|
+
<span className="msg-system-dot" />
|
|
128
|
+
<span>{typeof content === 'string' ? content : flattenText(content)}</span>
|
|
129
|
+
{ts && <span className="msg-ts chat-muted">{ts}</span>}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isStreaming = phase !== 'idle';
|
|
135
|
+
const text = flattenText(content);
|
|
136
|
+
|
|
137
|
+
if (role === 'user') {
|
|
138
|
+
return (
|
|
139
|
+
<div className="msg msg-user msg-enter">
|
|
140
|
+
<div className="msg-bubble-user-wrap">
|
|
141
|
+
<div className="msg-actions msg-actions-user">
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="msg-action-btn"
|
|
145
|
+
title="Copy"
|
|
146
|
+
onClick={handleCopy}
|
|
147
|
+
>
|
|
148
|
+
<Copy size={13} aria-hidden />
|
|
149
|
+
<span className="msg-action-label">{copied ? 'Copied' : 'Copy'}</span>
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
className="msg-action-btn"
|
|
154
|
+
title="Edit"
|
|
155
|
+
onClick={() => onEdit?.(props)}
|
|
156
|
+
>
|
|
157
|
+
<Edit size={13} aria-hidden />
|
|
158
|
+
<span className="msg-action-label">Edit</span>
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="msg-bubble msg-bubble-user">{text}</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="msg-avatar msg-avatar-user">U</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className={`msg msg-assistant msg-enter msg-phase-${phase}`}>
|
|
170
|
+
<div className="msg-avatar msg-avatar-assistant">
|
|
171
|
+
<span className="msg-avatar-letter">{getInitial(agent)}</span>
|
|
172
|
+
{isStreaming && <span className={`msg-avatar-pulse phase-${phase}`} aria-hidden />}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="msg-body">
|
|
175
|
+
<div className="msg-meta">
|
|
176
|
+
{agent && <span className="msg-author">{agent}</span>}
|
|
177
|
+
{isStreaming && <StreamingIndicator phase={phase} />}
|
|
178
|
+
{ts && <span className="msg-ts chat-muted">{ts}</span>}
|
|
179
|
+
</div>
|
|
180
|
+
<div className="msg-bubble-wrap">
|
|
181
|
+
<div className="msg-actions msg-actions-assistant">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
className="msg-action-btn"
|
|
185
|
+
title="Copy"
|
|
186
|
+
onClick={handleCopy}
|
|
187
|
+
>
|
|
188
|
+
<Copy size={13} aria-hidden />
|
|
189
|
+
<span className="msg-action-label">{copied ? 'Copied' : 'Copy'}</span>
|
|
190
|
+
</button>
|
|
191
|
+
{phase === 'idle' && (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className="msg-action-btn"
|
|
195
|
+
title="Regenerate"
|
|
196
|
+
onClick={() => onRegenerate?.(props)}
|
|
197
|
+
>
|
|
198
|
+
<RefreshCw size={13} aria-hidden />
|
|
199
|
+
<span className="msg-action-label">Regenerate</span>
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className="msg-action-btn"
|
|
205
|
+
title="Read aloud"
|
|
206
|
+
onClick={() => onSpeak?.(props)}
|
|
207
|
+
>
|
|
208
|
+
<Volume2 size={13} aria-hidden />
|
|
209
|
+
<span className="msg-action-label">Speak</span>
|
|
210
|
+
</button>
|
|
211
|
+
<div className="msg-action-sep" />
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
className={`msg-action-btn msg-action-feedback${feedback === 'up' ? ' active' : ''}`}
|
|
215
|
+
title="Helpful"
|
|
216
|
+
onClick={() => handleFeedback('up')}
|
|
217
|
+
>
|
|
218
|
+
<ThumbsUp size={13} aria-hidden />
|
|
219
|
+
</button>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
className={`msg-action-btn msg-action-feedback${feedback === 'down' ? ' active' : ''}`}
|
|
223
|
+
title="Not helpful"
|
|
224
|
+
onClick={() => handleFeedback('down')}
|
|
225
|
+
>
|
|
226
|
+
<ThumbsDown size={13} aria-hidden />
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="msg-bubble msg-bubble-assistant">
|
|
230
|
+
{typeof content === 'string'
|
|
231
|
+
? <p>{content}</p>
|
|
232
|
+
: content.map((block, i) => {
|
|
233
|
+
if (block.type === 'p' || block.type === 'text') {
|
|
234
|
+
return <p key={i}>{block.text}</p>;
|
|
235
|
+
}
|
|
236
|
+
if (block.type === 'ul') {
|
|
237
|
+
return (
|
|
238
|
+
<ul key={i}>
|
|
239
|
+
{(block.items ?? []).map((it, j) => (
|
|
240
|
+
<li key={j}>{it}</li>
|
|
241
|
+
))}
|
|
242
|
+
</ul>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (block.type === 'code') {
|
|
246
|
+
return (
|
|
247
|
+
<pre key={i} className="msg-code">
|
|
248
|
+
<code>{block.code}</code>
|
|
249
|
+
</pre>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
})}
|
|
254
|
+
{isStreaming && <span className="msg-cursor" aria-hidden />}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/components/chat/SessionRowMenu.tsx — three-mode popover for a session rail row.
|
|
2
|
+
//
|
|
3
|
+
// Three modes:
|
|
4
|
+
// 'main' — Rename + Delete buttons
|
|
5
|
+
// 'rename' — Inline input with Cancel/Save
|
|
6
|
+
// 'confirm-delete' — Inline confirmation with Cancel/Delete
|
|
7
|
+
//
|
|
8
|
+
// Rendered via createPortal on document.body so it isn't clipped by
|
|
9
|
+
// the rail's overflow:auto scroll container. The parent row supplies
|
|
10
|
+
// the anchor's bounding rect (or the menu uses an anchor prop).
|
|
11
|
+
|
|
12
|
+
import { useEffect, useRef } from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
14
|
+
import { Edit, Trash2 } from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
export type SessionMenuMode = 'main' | 'rename' | 'confirm-delete';
|
|
17
|
+
|
|
18
|
+
export interface SessionRowMenuAnchor {
|
|
19
|
+
/** Bounding rect of the trigger / row used to position the popover. */
|
|
20
|
+
rect: DOMRect;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionRowMenuProps {
|
|
24
|
+
session: { id: string; title: string };
|
|
25
|
+
mode: SessionMenuMode;
|
|
26
|
+
anchor: SessionRowMenuAnchor;
|
|
27
|
+
renameDraft: string;
|
|
28
|
+
setRenameDraft: (s: string) => void;
|
|
29
|
+
onEdit: () => void;
|
|
30
|
+
onDeleteRequest: () => void;
|
|
31
|
+
onConfirmDelete: () => void;
|
|
32
|
+
onConfirmRename: () => void;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SessionRowMenu({
|
|
37
|
+
session,
|
|
38
|
+
mode,
|
|
39
|
+
anchor,
|
|
40
|
+
renameDraft,
|
|
41
|
+
setRenameDraft,
|
|
42
|
+
onEdit,
|
|
43
|
+
onDeleteRequest,
|
|
44
|
+
onConfirmDelete,
|
|
45
|
+
onConfirmRename,
|
|
46
|
+
onClose,
|
|
47
|
+
}: SessionRowMenuProps) {
|
|
48
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
49
|
+
|
|
50
|
+
// Position the popover just below the row's right edge.
|
|
51
|
+
const top = Math.round(anchor.rect.bottom + 2);
|
|
52
|
+
const right = Math.max(8, Math.round(window.innerWidth - anchor.rect.right));
|
|
53
|
+
|
|
54
|
+
// Close on outside click + Esc.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const onDown = (e: MouseEvent) => {
|
|
57
|
+
const t = e.target as Node | null;
|
|
58
|
+
if (!ref.current || !t) return;
|
|
59
|
+
if (ref.current.contains(t)) return;
|
|
60
|
+
onClose();
|
|
61
|
+
};
|
|
62
|
+
const onKey = (e: KeyboardEvent) => {
|
|
63
|
+
if (e.key === 'Escape') onClose();
|
|
64
|
+
};
|
|
65
|
+
// Defer attaching the outside-click handler so the click that opened
|
|
66
|
+
// the menu doesn't immediately close it.
|
|
67
|
+
const id = window.setTimeout(() => {
|
|
68
|
+
document.addEventListener('mousedown', onDown);
|
|
69
|
+
document.addEventListener('keydown', onKey);
|
|
70
|
+
}, 0);
|
|
71
|
+
return () => {
|
|
72
|
+
window.clearTimeout(id);
|
|
73
|
+
document.removeEventListener('mousedown', onDown);
|
|
74
|
+
document.removeEventListener('keydown', onKey);
|
|
75
|
+
};
|
|
76
|
+
}, [onClose]);
|
|
77
|
+
|
|
78
|
+
// Auto-focus the rename input when entering rename mode.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (mode !== 'rename') return;
|
|
81
|
+
const id = window.setTimeout(() => {
|
|
82
|
+
const input = ref.current?.querySelector<HTMLInputElement>('input.session-row-menu-input');
|
|
83
|
+
input?.focus();
|
|
84
|
+
input?.select();
|
|
85
|
+
}, 0);
|
|
86
|
+
return () => window.clearTimeout(id);
|
|
87
|
+
}, [mode]);
|
|
88
|
+
|
|
89
|
+
const body = (
|
|
90
|
+
<div
|
|
91
|
+
ref={ref}
|
|
92
|
+
className="session-row-menu"
|
|
93
|
+
role="menu"
|
|
94
|
+
aria-label={`Options for ${session.title}`}
|
|
95
|
+
style={{ position: 'fixed', top, right }}
|
|
96
|
+
onClick={(e) => e.stopPropagation()}
|
|
97
|
+
>
|
|
98
|
+
{mode === 'main' && (
|
|
99
|
+
<>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="session-row-menu-item"
|
|
103
|
+
role="menuitem"
|
|
104
|
+
onClick={(e) => {
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
onEdit();
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<Edit size={13} aria-hidden />
|
|
110
|
+
<span>Rename</span>
|
|
111
|
+
<kbd>R</kbd>
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className="session-row-menu-item session-row-menu-item-danger"
|
|
116
|
+
role="menuitem"
|
|
117
|
+
onClick={(e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
onDeleteRequest();
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Trash2 size={13} aria-hidden />
|
|
123
|
+
<span>Delete</span>
|
|
124
|
+
<kbd>⌫</kbd>
|
|
125
|
+
</button>
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
{mode === 'rename' && (
|
|
129
|
+
<div className="session-row-menu-inline">
|
|
130
|
+
<div className="session-row-menu-inline-label chat-mono">Rename session</div>
|
|
131
|
+
<input
|
|
132
|
+
type="text"
|
|
133
|
+
className="session-row-menu-input"
|
|
134
|
+
value={renameDraft}
|
|
135
|
+
onChange={(e) => setRenameDraft(e.target.value)}
|
|
136
|
+
onKeyDown={(e) => {
|
|
137
|
+
if (e.key === 'Enter') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
onConfirmRename();
|
|
140
|
+
} else if (e.key === 'Escape') {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
onClose();
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
onClick={(e) => e.stopPropagation()}
|
|
146
|
+
aria-label="New session name"
|
|
147
|
+
/>
|
|
148
|
+
<div className="session-row-menu-inline-actions">
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
className="btn btn-ghost"
|
|
152
|
+
onClick={(e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
onClose();
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
Cancel
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className="btn btn-primary"
|
|
162
|
+
onClick={(e) => {
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
onConfirmRename();
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
Save
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
{mode === 'confirm-delete' && (
|
|
173
|
+
<div className="session-row-menu-inline">
|
|
174
|
+
<div className="session-row-menu-inline-label chat-mono">Delete this session?</div>
|
|
175
|
+
<div className="session-row-menu-inline-message">
|
|
176
|
+
"{session.title}" and its sub-agents will be removed.
|
|
177
|
+
</div>
|
|
178
|
+
<div className="session-row-menu-inline-actions">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
className="btn btn-ghost"
|
|
182
|
+
onClick={(e) => {
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
onClose();
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
Cancel
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="btn btn-danger"
|
|
192
|
+
onClick={(e) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
onConfirmDelete();
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
Delete
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return createPortal(body, document.body);
|
|
206
|
+
}
|
|
@@ -1,9 +1,37 @@
|
|
|
1
|
-
// src/components/chat/StreamingIndicator.tsx —
|
|
1
|
+
// src/components/chat/StreamingIndicator.tsx — phase indicator in the message meta row.
|
|
2
|
+
//
|
|
3
|
+
// Phase-driven label + 3-dot bounce. The dot animation duration
|
|
4
|
+
// shortens as the message progresses (think → stream → commit), so
|
|
5
|
+
// the visual rhythm matches the streaming cadence.
|
|
6
|
+
//
|
|
7
|
+
// The named export `StreamingIndicator` keeps its signature backward-
|
|
8
|
+
// compatible: argumentless → defaults to "think" phase (legacy
|
|
9
|
+
// dot-bounce used by WelcomeScreen + the old MessageBubble).
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
import type { StreamingPhase } from './MessageBlock';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Streaming phase. Defaults to 'think' for the legacy
|
|
15
|
+
* argumentless call site. */
|
|
16
|
+
phase?: StreamingPhase;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LABELS: Record<Exclude<StreamingPhase, 'idle'>, string> = {
|
|
20
|
+
think: 'thinking',
|
|
21
|
+
stream: 'streaming tokens',
|
|
22
|
+
commit: 'committing',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function StreamingIndicator({ phase = 'think' }: Props) {
|
|
26
|
+
const label = phase === 'idle' ? null : LABELS[phase];
|
|
4
27
|
return (
|
|
5
|
-
<
|
|
6
|
-
<span
|
|
7
|
-
|
|
28
|
+
<span className={`msg-stream-indicator phase-${phase}`}>
|
|
29
|
+
{label && <span className="msg-stream-label">{label}</span>}
|
|
30
|
+
<span className="msg-stream-dots" aria-hidden>
|
|
31
|
+
<span />
|
|
32
|
+
<span />
|
|
33
|
+
<span />
|
|
34
|
+
</span>
|
|
35
|
+
</span>
|
|
8
36
|
);
|
|
9
37
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/components/chat/_legacy.ts — backward-compatibility shim.
|
|
2
|
+
//
|
|
3
|
+
// MobileChat.tsx (mobile view) and any other consumer that imports the
|
|
4
|
+
// original names (`SessionList`, `InfoPanel`, `FloatingComposer`)
|
|
5
|
+
// continues to work even after the v3.22 redesign renamed these
|
|
6
|
+
// components. Each renamed component keeps its NEW default export in
|
|
7
|
+
// the new file; this module re-exports them under the OLD names.
|
|
8
|
+
//
|
|
9
|
+
// Notes for future maintainers:
|
|
10
|
+
// - `ChatTopBar`, `ChatThread`, `useChat`, `useSlashCommands` keep
|
|
11
|
+
// their original filenames + exports; nothing to shim here.
|
|
12
|
+
// - `SessionList` → ChatRail
|
|
13
|
+
// - `InfoPanel` → ChatInfoPanel
|
|
14
|
+
// - `FloatingComposer` → ChatComposer
|
|
15
|
+
// - `MessageBubble` / `StreamingIndicator` / `AgentChip` / `WelcomeScreen`
|
|
16
|
+
// / `SuggestionCards` / `Composer` / `FirstRunGreeting` / `ConfirmModal`
|
|
17
|
+
// / `EmptyState` / `LoadingSkeleton` / `useAutoGrowTextarea` are
|
|
18
|
+
// unchanged and continue to be exported from `index.ts`.
|
|
19
|
+
|
|
20
|
+
export { ChatRail as SessionList } from './ChatRail';
|
|
21
|
+
export { ChatInfoPanel as InfoPanel } from './ChatInfoPanel';
|
|
22
|
+
export { ChatComposer as FloatingComposer } from './ChatComposer';
|
|
23
|
+
|
|
24
|
+
// Re-export the unchanged pieces too so this file is a single drop-in
|
|
25
|
+
// for any consumer that wants to keep using old names in one place.
|
|
26
|
+
export { ChatTopBar } from './ChatTopBar';
|
|
27
|
+
export { ChatThread } from './ChatThread';
|
|
28
|
+
export { useChat } from './useChat';
|
|
29
|
+
export { useSlashCommands } from './useSlashCommands';
|
|
30
|
+
export type { SlashCommand } from './useSlashCommands';
|
|
@@ -3,15 +3,26 @@
|
|
|
3
3
|
export { ChatTopBar } from './ChatTopBar';
|
|
4
4
|
export { ChatThread } from './ChatThread';
|
|
5
5
|
export { MessageBubble } from './MessageBubble';
|
|
6
|
+
export { MessageBlock } from './MessageBlock';
|
|
6
7
|
export { WelcomeScreen } from './WelcomeScreen';
|
|
7
8
|
export { SuggestionCards } from './SuggestionCards';
|
|
8
9
|
export { Composer } from './Composer';
|
|
9
10
|
export { FloatingComposer } from './FloatingComposer';
|
|
11
|
+
export { ChatComposer } from './ChatComposer';
|
|
10
12
|
export { AgentChip } from './AgentChip';
|
|
11
13
|
export { SessionList } from './SessionList';
|
|
14
|
+
export { ChatRail } from './ChatRail';
|
|
15
|
+
export type { DisplaySession, SessionState } from './ChatRail';
|
|
12
16
|
export { InfoPanel } from './InfoPanel';
|
|
17
|
+
export { ChatInfoPanel } from './ChatInfoPanel';
|
|
13
18
|
export { LoadingSkeleton } from './LoadingSkeleton';
|
|
14
19
|
export { StreamingIndicator } from './StreamingIndicator';
|
|
20
|
+
export { JumpToLatest } from './JumpToLatest';
|
|
21
|
+
export { AgentNode } from './AgentNode';
|
|
22
|
+
export type { AgentTreeNode, AgentStatus } from './AgentNode';
|
|
23
|
+
export { AgentTree, SubAgentList } from './AgentTree';
|
|
24
|
+
export { SessionRowMenu } from './SessionRowMenu';
|
|
25
|
+
export type { SessionMenuMode, SessionRowMenuAnchor, SessionRowMenuProps } from './SessionRowMenu';
|
|
15
26
|
export { ConfirmModal } from './ConfirmModal';
|
|
16
27
|
export { EmptyState } from './EmptyState';
|
|
17
28
|
export { useChat } from './useChat';
|