@polderlabs/bizar 4.4.13 → 4.5.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.
Files changed (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -1,29 +1,77 @@
1
- // src/components/chat/ChatInfoPanel.tsx — right info panel (v3.22 design).
1
+ // src/components/chat/ChatInfoPanel.tsx — right info panel (v4.2.5).
2
2
  //
3
- // Replaces InfoPanel. Layout follows the design HTML's sectioned
4
- // info panel: Session / Project / Model / Tokens / Cost / Attached
5
- // agents / Linked.
6
- //
7
- // Props match the original InfoPanel EXACTLY (so Chat.tsx doesn't need
8
- // any changes).
3
+ // v4.2.5 overhauled: shows session metadata (id, agent, model,
4
+ // provider, message count), the current `activeSource`, the model
5
+ // in use, a section for usage/cost placeholders (filled in by the
6
+ // MiniMax usage module when present), and rename / delete / export
7
+ // actions wired to the parent.
9
8
 
10
- import { MessageSquare, Bot, Server, Terminal } from 'lucide-react';
9
+ import { useEffect, useState } from 'react';
10
+ import {
11
+ MessageSquare,
12
+ Bot,
13
+ Server,
14
+ Terminal,
15
+ Pencil,
16
+ Trash2,
17
+ Download,
18
+ CircleDollarSign,
19
+ } from 'lucide-react';
20
+ import type { ChatMessage } from '../../lib/types';
11
21
 
12
22
  interface Props {
13
23
  sessionId: string;
14
- messages: Array<unknown>;
24
+ messages: ChatMessage[];
15
25
  pinned: Set<number>;
16
26
  agent: string;
17
27
  model: string;
18
28
  agents: Array<{ name: string; model?: string; mode?: string }>;
19
29
  mcps: Array<{ id: string; command?: string; enabled?: boolean }>;
20
30
  allCommands: Array<{ cmd: string; desc: string; mod?: string }>;
31
+ /** Which stream is currently displayed. */
32
+ activeSource?: 'bizar' | 'opencode' | null;
33
+ /** Optional rename/delete/export handlers wired by Chat.tsx. */
34
+ onRename?: () => void;
35
+ onDelete?: () => void;
36
+ onExport?: () => void;
37
+ /** Per-action busy flags — disable buttons while pending. */
38
+ busy?: { rename?: boolean; delete?: boolean };
39
+ }
40
+
41
+ interface UsageRow {
42
+ promptTokens: number;
43
+ completionTokens: number;
44
+ totalTokens: number;
45
+ costUsd: number;
21
46
  }
22
47
 
23
48
  function formatNumber(n: number): string {
24
49
  return n.toLocaleString();
25
50
  }
26
51
 
52
+ /** Map a model id like "opencode/deepseek-v4-flash-free" → "opencode". */
53
+ function providerFromModel(model: string): string {
54
+ if (!model) return '';
55
+ if (!model.includes('/')) return '';
56
+ return model.split('/')[0];
57
+ }
58
+
59
+ /** Map a model id to a cost (USD/M-token) — placeholder rates; the
60
+ * MiniMax usage module supplies real numbers where available. */
61
+ function approxCostPer1M(model: string): number {
62
+ if (!model) return 0;
63
+ const m = model.toLowerCase();
64
+ if (m.includes('opus')) return 15;
65
+ if (m.includes('sonnet')) return 3;
66
+ if (m.includes('haiku')) return 0.8;
67
+ if (m.includes('gpt-4o')) return 5;
68
+ if (m.includes('gpt-4')) return 10;
69
+ if (m.includes('deepseek')) return 0.27;
70
+ if (m.includes('minimax')) return 0.3;
71
+ if (m.includes('llama-3.3-70b')) return 0.59;
72
+ return 1;
73
+ }
74
+
27
75
  export function ChatInfoPanel({
28
76
  sessionId,
29
77
  messages,
@@ -33,22 +81,74 @@ export function ChatInfoPanel({
33
81
  agents,
34
82
  mcps,
35
83
  allCommands,
84
+ activeSource = 'bizar',
85
+ onRename,
86
+ onDelete,
87
+ onExport,
88
+ busy,
36
89
  }: Props) {
37
- // Tokens + cost are stubbed the dashboard doesn't currently
38
- // surface these from the chat backend. Show the count of messages
39
- // + pinned as a sensible default and leave token/cost placeholders
40
- // clearly marked so it's obvious they're not yet wired.
90
+ // Best-effort: pull usage summary from the MiniMax usage endpoint
91
+ // when the session is opencode. Falls back silently if the module
92
+ // isn't installed yet.
93
+ const [usage, setUsage] = useState<UsageRow | null>(null);
94
+ useEffect(() => {
95
+ let cancelled = false;
96
+ (async () => {
97
+ try {
98
+ const res = await fetch('/api/usage?range=24h', {
99
+ headers: { Accept: 'application/json' },
100
+ });
101
+ if (!res.ok) return;
102
+ const body = (await res.json()) as UsageRow;
103
+ if (!cancelled) setUsage(body);
104
+ } catch {
105
+ /* ignore — usage is non-essential */
106
+ }
107
+ })();
108
+ return () => {
109
+ cancelled = true;
110
+ };
111
+ }, [sessionId]);
112
+
41
113
  const msgCount = messages?.length ?? 0;
42
114
  const pinnedCount = pinned?.size ?? 0;
43
- const tokensUsed = 0;
115
+
116
+ // Estimate tokens / cost from message length when no real usage
117
+ // data is available. Real numbers come from the MiniMax module.
118
+ const totalChars = messages.reduce(
119
+ (n, m) => n + ((m.content || m.message || '') as string).length,
120
+ 0,
121
+ );
122
+ const approxTokens = Math.round(totalChars / 4);
123
+ const rate = approxCostPer1M(model);
124
+ const approxCostUsd = (approxTokens / 1_000_000) * rate;
125
+ const tokensUsed = usage?.totalTokens ?? approxTokens;
126
+ const costUsd = usage?.costUsd ?? approxCostUsd;
44
127
  const tokensTotal = 128_000;
45
- const costUsd = 0;
128
+ const provider = providerFromModel(model);
46
129
 
47
130
  return (
48
131
  <aside className="chat-info">
49
132
  <div className="chat-info-section">
50
- <h4>Session</h4>
51
- <div>{sessionId || 'Live'}</div>
133
+ <div className="chat-info-section-head">
134
+ <h4>
135
+ <MessageSquare
136
+ size={11}
137
+ style={{ marginRight: 4, verticalAlign: -1 }}
138
+ aria-hidden
139
+ />{' '}
140
+ Session
141
+ </h4>
142
+ <span
143
+ className={`chat-source-badge chat-source-${activeSource ?? 'none'}`}
144
+ style={{ marginLeft: 'auto' }}
145
+ >
146
+ {activeSource === 'opencode' ? 'opencode' : 'bizar chat'}
147
+ </span>
148
+ </div>
149
+ <div className="chat-info-mono chat-ellipsis" title={sessionId}>
150
+ {sessionId || 'Live'}
151
+ </div>
52
152
  <div className="chat-info-mono">
53
153
  {formatNumber(msgCount)} message{msgCount === 1 ? '' : 's'}
54
154
  {pinnedCount > 0 && ` · ${pinnedCount} pinned`}
@@ -56,15 +156,24 @@ export function ChatInfoPanel({
56
156
  </div>
57
157
 
58
158
  <div className="chat-info-section">
59
- <h4>Agent</h4>
60
- <div>{agent || ''}</div>
159
+ <h4>
160
+ <Bot size={11} style={{ marginRight: 4, verticalAlign: -1 }} aria-hidden />{' '}
161
+ Agent
162
+ </h4>
163
+ <div className="chat-info-value">{agent || '—'}</div>
61
164
  </div>
62
165
 
63
166
  <div className="chat-info-section">
64
- <h4>Model</h4>
167
+ <h4>
168
+ <Server size={11} style={{ marginRight: 4, verticalAlign: -1 }} aria-hidden />{' '}
169
+ Model
170
+ </h4>
65
171
  <div className="chat-mono chat-ellipsis" title={model}>
66
172
  {model || '—'}
67
173
  </div>
174
+ {provider && (
175
+ <div className="chat-info-mono">provider · {provider}</div>
176
+ )}
68
177
  </div>
69
178
 
70
179
  <div className="chat-info-section">
@@ -75,20 +184,36 @@ export function ChatInfoPanel({
75
184
  <div className="chat-info-bar" aria-hidden>
76
185
  <div
77
186
  className="chat-info-bar-fill"
78
- style={{ width: `${Math.min(100, (tokensUsed / Math.max(1, tokensTotal)) * 100)}%` }}
187
+ style={{
188
+ width: `${Math.min(100, (tokensUsed / Math.max(1, tokensTotal)) * 100)}%`,
189
+ }}
79
190
  />
80
191
  </div>
81
192
  </div>
82
193
 
83
194
  <div className="chat-info-section">
84
- <h4>Cost</h4>
195
+ <h4>
196
+ <CircleDollarSign
197
+ size={11}
198
+ style={{ marginRight: 4, verticalAlign: -1 }}
199
+ aria-hidden
200
+ />{' '}
201
+ Cost
202
+ </h4>
85
203
  <div className="chat-mono">${costUsd.toFixed(4)}</div>
204
+ {!usage && (
205
+ <div className="chat-info-mono">approx · live from MiniMax</div>
206
+ )}
86
207
  </div>
87
208
 
88
209
  <div className="chat-info-section">
89
210
  <h4>
90
- <Bot size={11} style={{ marginRight: 4, verticalAlign: -1 }} aria-hidden /> Attached
91
- agents
211
+ <Bot
212
+ size={11}
213
+ style={{ marginRight: 4, verticalAlign: -1 }}
214
+ aria-hidden
215
+ />{' '}
216
+ Attached agents
92
217
  </h4>
93
218
  {agents?.length ? (
94
219
  <div className="chat-info-agents">
@@ -105,7 +230,12 @@ export function ChatInfoPanel({
105
230
 
106
231
  <div className="chat-info-section">
107
232
  <h4>
108
- <Server size={11} style={{ marginRight: 4, verticalAlign: -1 }} aria-hidden /> MCPs
233
+ <Server
234
+ size={11}
235
+ style={{ marginRight: 4, verticalAlign: -1 }}
236
+ aria-hidden
237
+ />{' '}
238
+ MCPs
109
239
  </h4>
110
240
  {mcps?.length ? (
111
241
  <div className="chat-info-agents">
@@ -122,23 +252,55 @@ export function ChatInfoPanel({
122
252
 
123
253
  <div className="chat-info-section">
124
254
  <h4>
125
- <Terminal size={11} style={{ marginRight: 4, verticalAlign: -1 }} aria-hidden /> Slash
126
- commands
127
- </h4>
128
- <div className="chat-info-mono">{allCommands?.length ?? 0} available</div>
129
- </div>
130
-
131
- <div className="chat-info-section">
132
- <h4>
133
- <MessageSquare
255
+ <Terminal
134
256
  size={11}
135
257
  style={{ marginRight: 4, verticalAlign: -1 }}
136
258
  aria-hidden
137
259
  />{' '}
138
- Linked
260
+ Slash commands
139
261
  </h4>
140
- <div className="chat-info-mono">No links yet</div>
262
+ <div className="chat-info-mono">{allCommands?.length ?? 0} available</div>
141
263
  </div>
264
+
265
+ {(onRename || onDelete || onExport) && (
266
+ <div className="chat-info-section">
267
+ <h4>Actions</h4>
268
+ <div className="chat-info-actions">
269
+ {onRename && (
270
+ <button
271
+ type="button"
272
+ className="btn btn-ghost btn-sm"
273
+ onClick={onRename}
274
+ disabled={busy?.rename}
275
+ title="Rename session"
276
+ >
277
+ <Pencil size={12} aria-hidden /> Rename
278
+ </button>
279
+ )}
280
+ {onExport && (
281
+ <button
282
+ type="button"
283
+ className="btn btn-ghost btn-sm"
284
+ onClick={onExport}
285
+ title="Export transcript"
286
+ >
287
+ <Download size={12} aria-hidden /> Export
288
+ </button>
289
+ )}
290
+ {onDelete && (
291
+ <button
292
+ type="button"
293
+ className="btn btn-ghost btn-sm btn-danger"
294
+ onClick={onDelete}
295
+ disabled={busy?.delete}
296
+ title="Delete session"
297
+ >
298
+ <Trash2 size={12} aria-hidden /> Delete
299
+ </button>
300
+ )}
301
+ </div>
302
+ </div>
303
+ )}
142
304
  </aside>
143
305
  );
144
- }
306
+ }
@@ -1,4 +1,8 @@
1
1
  // src/components/chat/ChatThread.tsx — message list, welcome screen, loading skeleton.
2
+ //
3
+ // v4.2.5 — accepts `activeSource` so the loading skeleton can hint
4
+ // which stream is loading and the welcome variant for "opencode"
5
+ // can differ from the local chat welcome.
2
6
 
3
7
  import { useEffect, useRef } from 'react';
4
8
  import { LoadingSkeleton } from './LoadingSkeleton';
@@ -19,6 +23,12 @@ interface Props {
19
23
  activeProject: { name: string } | null;
20
24
  sessionId: string;
21
25
  pinned: Set<number>;
26
+ /** Which stream currently owns the thread — purely cosmetic, lets
27
+ * the empty/loading states be slightly more specific. The type
28
+ * uses `unknown` so we don't pull in a tight coupling; the value
29
+ * is reflected back into the loading skeleton and welcome screen
30
+ * via data attributes so CSS can style accordingly. */
31
+ activeSource?: 'bizar' | 'opencode' | null;
22
32
  onPickSuggestion: (text: string) => void;
23
33
  onCopy: (m: Message) => void;
24
34
  onDelete: (idx: number) => void;
@@ -32,6 +42,7 @@ export function ChatThread({
32
42
  activeProject,
33
43
  sessionId,
34
44
  pinned,
45
+ activeSource,
35
46
  onPickSuggestion,
36
47
  onCopy,
37
48
  onDelete,
@@ -40,19 +51,12 @@ export function ChatThread({
40
51
  }: Props) {
41
52
  const innerRef = useRef<HTMLDivElement>(null);
42
53
 
43
- // v3.22 — only auto-scroll when we own the scroll container (i.e.
44
- // when rendered directly as the message scroller). When embedded
54
+ // Only auto-scroll when we own the scroll container (i.e. when
55
+ // rendered directly as the message scroller). When embedded
45
56
  // inside Chat.tsx's chat-thread-scroll, the parent handles scroll.
46
- // We detect this by checking if our closest scroll ancestor is
47
- // ourselves; if so, auto-scroll. Otherwise, no-op.
48
57
  useEffect(() => {
49
58
  const el = innerRef.current;
50
59
  if (!el) return;
51
- // If we ARE the scroll container (legacy / mobile path), scroll.
52
- // If a parent .chat-thread-scroll wraps us, the parent scrolls.
53
- const isScroller = el.scrollHeight > el.clientHeight && el.matches(':scope');
54
- // Simpler heuristic: if no `.chat-thread-scroll` ancestor exists,
55
- // auto-scroll. Otherwise let the parent handle it.
56
60
  let p: HTMLElement | null = el.parentElement;
57
61
  let wrapped = false;
58
62
  while (p) {
@@ -63,13 +67,13 @@ export function ChatThread({
63
67
  p = p.parentElement;
64
68
  }
65
69
  if (!wrapped) el.scrollTop = el.scrollHeight;
66
- // isScroller is referenced for clarity; suppress unused warning.
67
- void isScroller;
68
70
  }, [messages]);
69
71
 
72
+ const stream: 'bizar' | 'opencode' = activeSource === 'opencode' ? 'opencode' : 'bizar';
73
+
70
74
  if (loading) {
71
75
  return (
72
- <div className="chat-thread legacy" ref={innerRef}>
76
+ <div className="chat-thread legacy" ref={innerRef} data-stream={stream}>
73
77
  <LoadingSkeleton count={3} />
74
78
  </div>
75
79
  );
@@ -77,16 +81,24 @@ export function ChatThread({
77
81
 
78
82
  if (!activeProject) {
79
83
  return (
80
- <div className="chat-thread legacy" ref={innerRef}>
81
- <WelcomeScreen variant="no-project" projectName="" onPickSuggestion={onPickSuggestion} />
84
+ <div className="chat-thread legacy" ref={innerRef} data-stream={stream}>
85
+ <WelcomeScreen
86
+ variant="no-project"
87
+ projectName=""
88
+ onPickSuggestion={onPickSuggestion}
89
+ />
82
90
  </div>
83
91
  );
84
92
  }
85
93
 
86
94
  if (messages.length === 0) {
87
95
  return (
88
- <div className="chat-thread legacy" ref={innerRef}>
89
- <WelcomeScreen variant="empty" projectName={activeProject.name} onPickSuggestion={onPickSuggestion} />
96
+ <div className="chat-thread legacy" ref={innerRef} data-stream={stream}>
97
+ <WelcomeScreen
98
+ variant="empty"
99
+ projectName={activeProject.name}
100
+ onPickSuggestion={onPickSuggestion}
101
+ />
90
102
  </div>
91
103
  );
92
104
  }
@@ -106,4 +118,4 @@ export function ChatThread({
106
118
  ))}
107
119
  </div>
108
120
  );
109
- }
121
+ }
@@ -19,11 +19,17 @@ interface Props {
19
19
  onAttach: () => void;
20
20
  sessionsOpen: boolean;
21
21
  infoOpen: boolean;
22
+ /** Which stream will receive the next message — propagates the
23
+ * pill border color and a small "→ opencode" hint. */
24
+ activeSource?: 'bizar' | 'opencode' | null;
22
25
  }
23
26
 
24
27
  export function FloatingComposer(props: Props) {
28
+ const { activeSource } = props;
25
29
  return (
26
- <div className="chat-composer-wrap">
30
+ <div
31
+ className={`chat-composer-wrap chat-composer-source-${activeSource ?? 'none'} legacy`}
32
+ >
27
33
  <div className="chat-composer-pill">
28
34
  <Composer {...props} />
29
35
  </div>
@@ -1,7 +1,19 @@
1
- // src/components/chat/InfoPanel.tsx — compact 4-card info sidebar.
1
+ // src/components/chat/InfoPanel.tsx — mobile-compact info sidebar.
2
+ //
3
+ // v4.2.5 — overhauled: shows activeSource badge, message count,
4
+ // session metadata, model/provider, and rename/delete actions for
5
+ // the active session.
2
6
 
3
7
  import { useState } from 'react';
4
- import { MessageSquare, Bot, Server, Terminal, ChevronDown } from 'lucide-react';
8
+ import {
9
+ MessageSquare,
10
+ Bot,
11
+ Server,
12
+ Terminal,
13
+ ChevronDown,
14
+ Trash2,
15
+ Pencil,
16
+ } from 'lucide-react';
5
17
 
6
18
  interface Props {
7
19
  sessionId: string;
@@ -12,6 +24,11 @@ interface Props {
12
24
  agents: Array<{ name: string; model?: string; mode?: string }>;
13
25
  mcps: Array<{ id: string; command?: string; enabled?: boolean }>;
14
26
  allCommands: Array<{ cmd: string; desc: string; mod?: string }>;
27
+ activeSource?: 'bizar' | 'opencode' | null;
28
+ /** Optional rename handler — when omitted the rename button is hidden. */
29
+ onRename?: () => void;
30
+ /** Optional delete handler — when omitted the delete button is hidden. */
31
+ onDelete?: () => void;
15
32
  }
16
33
 
17
34
  export function InfoPanel({
@@ -23,6 +40,9 @@ export function InfoPanel({
23
40
  agents,
24
41
  mcps,
25
42
  allCommands,
43
+ activeSource = 'bizar',
44
+ onRename,
45
+ onDelete,
26
46
  }: Props) {
27
47
  const [showAllAgents, setShowAllAgents] = useState(false);
28
48
  const [showCommands, setShowCommands] = useState(false);
@@ -30,11 +50,21 @@ export function InfoPanel({
30
50
  const visibleAgents = showAllAgents ? agents : agents.slice(0, 4);
31
51
  const visibleCommands = showCommands ? allCommands : allCommands.slice(0, 4);
32
52
 
53
+ // Derive a friendly provider from the model id (e.g.
54
+ // "opencode/deepseek-v4-flash-free" → provider "opencode").
55
+ const provider = model.includes('/') ? model.split('/')[0] : '';
56
+
33
57
  return (
34
58
  <div className="chat-info">
35
59
  <div className="chat-info-card">
36
60
  <div className="chat-info-card-title">
37
61
  <MessageSquare size={14} /> Session
62
+ <span
63
+ className={`chat-source-badge chat-source-${activeSource ?? 'none'}`}
64
+ style={{ marginLeft: 'auto' }}
65
+ >
66
+ {activeSource === 'opencode' ? 'opencode' : 'bizar chat'}
67
+ </span>
38
68
  </div>
39
69
  <dl className="env-table">
40
70
  <dt>id</dt>
@@ -46,8 +76,38 @@ export function InfoPanel({
46
76
  <dt>Agent</dt>
47
77
  <dd className="mono">{agent || '—'}</dd>
48
78
  <dt>Model</dt>
49
- <dd className="mono ellipsis" title={model}>{model || '—'}</dd>
79
+ <dd className="mono ellipsis" title={model}>
80
+ {model || '—'}
81
+ </dd>
82
+ {provider && (
83
+ <>
84
+ <dt>Provider</dt>
85
+ <dd className="mono">{provider}</dd>
86
+ </>
87
+ )}
50
88
  </dl>
89
+ {(onRename || onDelete) && (
90
+ <div className="chat-info-actions">
91
+ {onRename && (
92
+ <button
93
+ type="button"
94
+ className="btn btn-ghost btn-sm"
95
+ onClick={onRename}
96
+ >
97
+ <Pencil size={12} aria-hidden /> Rename
98
+ </button>
99
+ )}
100
+ {onDelete && (
101
+ <button
102
+ type="button"
103
+ className="btn btn-ghost btn-danger btn-sm"
104
+ onClick={onDelete}
105
+ >
106
+ <Trash2 size={12} aria-hidden /> Delete
107
+ </button>
108
+ )}
109
+ </div>
110
+ )}
51
111
  </div>
52
112
 
53
113
  <div className="chat-info-card">
@@ -80,7 +140,9 @@ export function InfoPanel({
80
140
  <span className="chat-info-card-count">{mcps.length}</span>
81
141
  </div>
82
142
  <ul className="mod-mini-list">
83
- {mcps.length === 0 ? <li className="muted">No MCPs configured.</li> : null}
143
+ {mcps.length === 0 ? (
144
+ <li className="muted">No MCPs configured.</li>
145
+ ) : null}
84
146
  {mcps.map((m) => (
85
147
  <li key={m.id} className="mod-mini">
86
148
  <span className="mod-mini-name">{m.id}</span>
@@ -101,7 +163,10 @@ export function InfoPanel({
101
163
  >
102
164
  <Terminal size={14} /> Slash commands
103
165
  <span className="chat-info-card-count">{allCommands.length}</span>
104
- <ChevronDown size={12} className={`chat-info-card-chevron ${showCommands ? 'open' : ''}`} />
166
+ <ChevronDown
167
+ size={12}
168
+ className={`chat-info-card-chevron ${showCommands ? 'open' : ''}`}
169
+ />
105
170
  </button>
106
171
  {showCommands && (
107
172
  <ul className="mod-mini-list">
@@ -116,4 +181,4 @@ export function InfoPanel({
116
181
  </div>
117
182
  </div>
118
183
  );
119
- }
184
+ }