@polderlabs/bizar 4.4.12 → 4.5.0

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 (91) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
  3. package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
  4. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
  6. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
  7. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
  8. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  12. package/bizar-dash/skills/bizar/SKILL.md +96 -0
  13. package/bizar-dash/skills/chat/SKILL.md +74 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +75 -0
  15. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  16. package/bizar-dash/skills/obsidian/SKILL.md +55 -0
  17. package/bizar-dash/skills/providers/SKILL.md +75 -0
  18. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  19. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  20. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  21. package/bizar-dash/skills/usage/SKILL.md +62 -0
  22. package/bizar-dash/src/server/api.mjs +12 -0
  23. package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
  24. package/bizar-dash/src/server/memory-store.mjs +38 -0
  25. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  26. package/bizar-dash/src/server/minimax.mjs +427 -110
  27. package/bizar-dash/src/server/providers-store.mjs +966 -6
  28. package/bizar-dash/src/server/routes/config.mjs +52 -1
  29. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  30. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  31. package/bizar-dash/src/server/routes/memory.mjs +241 -1
  32. package/bizar-dash/src/server/routes/minimax.mjs +50 -57
  33. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  34. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  35. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  36. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  37. package/bizar-dash/src/server/routes/update.mjs +340 -0
  38. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  39. package/bizar-dash/src/server/serve-info.mjs +135 -4
  40. package/bizar-dash/src/server/server.mjs +4 -0
  41. package/bizar-dash/src/server/skills-store.mjs +152 -262
  42. package/bizar-dash/src/web/App.tsx +118 -29
  43. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  44. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  45. package/bizar-dash/src/web/components/Topbar.tsx +0 -1
  46. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  47. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  48. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  49. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  50. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  51. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  52. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  53. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  54. package/bizar-dash/src/web/lib/api.ts +43 -0
  55. package/bizar-dash/src/web/main.tsx +1 -0
  56. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  57. package/bizar-dash/src/web/styles/chat.css +135 -1
  58. package/bizar-dash/src/web/styles/main.css +46 -0
  59. package/bizar-dash/src/web/styles/minimax-usage.css +471 -0
  60. package/bizar-dash/src/web/styles/settings.css +418 -0
  61. package/bizar-dash/src/web/styles/skills.css +302 -0
  62. package/bizar-dash/src/web/styles/tasks.css +288 -0
  63. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  64. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  65. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +620 -240
  66. package/bizar-dash/src/web/views/Settings.tsx +6 -0
  67. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  68. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  69. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  70. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  71. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  72. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  73. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  74. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  75. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  76. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  77. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  78. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  79. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  80. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  81. package/bizar-dash/tests/update-check.test.mjs +127 -0
  82. package/bizar-dash/tests/update-run.test.mjs +266 -0
  83. package/cli/bin.mjs +345 -4
  84. package/cli/provision.mjs +118 -4
  85. package/config/agents/_shared/SKILLS.md +109 -0
  86. package/package.json +1 -1
  87. package/bizar-dash/dist/assets/main-BKXEqU1w.css +0 -1
  88. package/bizar-dash/dist/assets/main-EK_fzXn_.js +0 -352
  89. package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +0 -1
  90. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  91. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -1,5 +1,13 @@
1
1
  // src/lib/api.ts — REST client. Single instance, type-safe wrappers.
2
2
  //
3
+ // v3.6.2 — Token URL pickup for reverse-proxy / Tailscale Serve access.
4
+ // When the dashboard is accessed via Tailscale Serve (or any reverse
5
+ // proxy) the browser runs on a different origin than localhost. The server
6
+ // sees the remote client through the local proxy and requires auth.
7
+ // The `?token=…` URL param lets operators share a direct link that
8
+ // automatically installs the token into localStorage so the dashboard
9
+ // "just works" after the first paste — no separate localhost tab needed.
10
+ //
3
11
  // v3.6.1 — Authentication is now loopback-aware on the server side.
4
12
  // The browser makes API calls over 127.0.0.1 (or via Tailscale serve
5
13
  // proxying to 127.0.0.1), and the server auto-trusts loopback
@@ -53,6 +61,35 @@ class ApiClient {
53
61
  }
54
62
  }
55
63
 
64
+ /**
65
+ * v3.6.2 — Check the URL search params for a `token` value and
66
+ * persist it into localStorage if found. Strips the param from the
67
+ * URL via `history.replaceState` so the token doesn't leak into
68
+ * browser history or shared links.
69
+ *
70
+ * Returns `true` if a token was found and saved, `false` otherwise.
71
+ * Safe to call multiple times (subsequent calls with no token in URL
72
+ * return `false` and are no-ops).
73
+ */
74
+ pickupTokenFromUrl(): boolean {
75
+ try {
76
+ const params = new URLSearchParams(window.location.search);
77
+ const tok = params.get('token');
78
+ if (!tok) return false;
79
+ this.setToken(tok);
80
+ // Remove the token param from the URL without a page reload.
81
+ params.delete('token');
82
+ const newSearch = params.toString();
83
+ const newUrl = newSearch
84
+ ? `${window.location.pathname}?${newSearch}`
85
+ : window.location.pathname;
86
+ history.replaceState(null, '', newUrl);
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
56
93
  /** Build the URL with the token appended as ?token=… if present. */
57
94
  urlWithToken(path: string): string {
58
95
  const url = this.base + path;
@@ -180,6 +217,12 @@ class ApiClient {
180
217
  }
181
218
 
182
219
  export const api = new ApiClient();
220
+
221
+ // v3.6.2 — Auto-pickup token from URL on module load so the dashboard
222
+ // works through reverse proxies (Tailscale Serve, nginx, etc.) without
223
+ // needing a separate localhost tab.
224
+ api.pickupTokenFromUrl();
225
+
183
226
  export { ApiClient, TOKEN_KEY };
184
227
 
185
228
  /**
@@ -9,6 +9,7 @@ import './styles/main.css';
9
9
  import './styles/mobile.css';
10
10
  import './styles/glyphs.css';
11
11
  import './styles/minimax-usage.css';
12
+ import './styles/skills.css';
12
13
 
13
14
  function Root() {
14
15
  const [isMobile, setIsMobile] = useState(() => {
@@ -1,5 +1,9 @@
1
- // src/web/mobile/views/MobileChat.tsx — mobile-adapted chat using shared components.
2
- // Composer state (agent, model, text, attachments) is local.
1
+ // src/web/mobile/views/MobileChat.tsx — mobile chat (v4.2.5).
2
+ //
3
+ // v4.2.5 — matches Chat.tsx overhaul: opencode session-create via the
4
+ // new endpoint, source-routed composer (opencode send vs. local chat),
5
+ // source indicator badge, rename / delete / export on the info panel,
6
+ // and matching busy flags.
3
7
 
4
8
  import { useEffect, useRef, useState } from 'react';
5
9
  import { ChatTopBar } from '../../components/chat/ChatTopBar';
@@ -22,28 +26,41 @@ interface Props {
22
26
  onClearTaskId?: () => void;
23
27
  }
24
28
 
25
- export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, onClearTaskId }: Props) {
29
+ export function MobileChat({
30
+ snapshot,
31
+ settings,
32
+ setActiveTab,
33
+ initialTaskId,
34
+ onClearTaskId,
35
+ }: Props) {
26
36
  const toast = useToast();
27
37
  const modal = useModal();
28
38
 
29
39
  const chat = useChat(snapshot, settings, initialTaskId ?? '');
40
+ useEffect(() => {
41
+ chat.setToast({
42
+ error: (msg: string) => toast.error(msg),
43
+ success: (msg: string) => toast.success(msg),
44
+ info: (msg: string) => toast.info(msg),
45
+ warning: (msg: string) => toast.warning(msg),
46
+ });
47
+ }, [chat, toast]);
30
48
 
31
49
  const [text, setText] = useState('');
32
50
  const [agent, setAgent] = useState(settings.defaultAgent || 'odin');
33
51
  const [model, setModel] = useState(settings.defaultModel || '');
34
52
  const [attachments, setAttachments] = useState<string[]>([]);
35
- const [creating, setCreating] = useState(false);
36
53
  const [sessionsOpen, setSessionsOpen] = useState(false);
37
54
  const [infoOpen, setInfoOpen] = useState(false);
38
55
 
39
56
  const fileInputRef = useRef<HTMLInputElement>(null);
40
57
 
41
58
  const { allCommands, suggestions, setQuery } = useSlashCommands(snapshot);
42
-
43
- useEffect(() => { setQuery(text); }, [text, setQuery]);
59
+ useEffect(() => {
60
+ setQuery(text);
61
+ }, [text, setQuery]);
44
62
 
45
63
  const onAttach = () => fileInputRef.current?.click();
46
-
47
64
  const onFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
48
65
  const files = e.target.files;
49
66
  if (!files) return;
@@ -56,30 +73,19 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
56
73
  if (fileInputRef.current) fileInputRef.current.value = '';
57
74
  };
58
75
 
59
- const handleSend = () => {
76
+ const handleSend = async () => {
60
77
  const msg = text.trim();
61
78
  if (!msg) return;
62
79
  setText('');
63
80
  setQuery('');
64
- chat.onSend(msg, agent, model, attachments);
81
+ const result = await chat.onSend(msg, agent, model, attachments);
82
+ if (result.ok) chat.jumpToLatest();
65
83
  };
66
84
 
67
85
  const handleCreateSession = async () => {
68
- if (creating) return;
69
- if (!snapshot.activeProject) {
70
- toast.warning('Pick a project in Overview to scope chat sessions.', 4000);
71
- return;
72
- }
73
- setCreating(true);
74
- try {
75
- const created = await fetch('/chat/sessions', { method: 'POST' }).then((r) => r.json());
76
- chat.loadChat(created.id);
77
- toast.success(`Session ${created.id} created.`);
78
- } catch (err) {
79
- toast.error(`Create failed: ${(err as Error).message}`);
80
- } finally {
81
- setCreating(false);
82
- }
86
+ if (chat.busy.create) return;
87
+ await chat.onCreateSession();
88
+ setSessionsOpen(false);
83
89
  };
84
90
 
85
91
  const handleDelete = (idx: number) => {
@@ -88,7 +94,9 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
88
94
  children: <p style={{ margin: 0 }}>This action cannot be undone.</p>,
89
95
  footer: (
90
96
  <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
91
- <Button variant="secondary" size="sm" onClick={() => modal.close()}>Cancel</Button>
97
+ <Button variant="secondary" size="sm" onClick={() => modal.close()}>
98
+ Cancel
99
+ </Button>
92
100
  <Button
93
101
  variant="danger"
94
102
  size="sm"
@@ -104,6 +112,34 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
104
112
  });
105
113
  };
106
114
 
115
+ const handleDeleteSession = (id: string, title: string) => {
116
+ modal.open({
117
+ title: 'Delete session?',
118
+ children: (
119
+ <p style={{ margin: 0 }}>
120
+ Delete <strong>{title}</strong>? This cannot be undone.
121
+ </p>
122
+ ),
123
+ footer: (
124
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
125
+ <Button variant="secondary" size="sm" onClick={() => modal.close()}>
126
+ Cancel
127
+ </Button>
128
+ <Button
129
+ variant="danger"
130
+ size="sm"
131
+ onClick={async () => {
132
+ modal.close();
133
+ await chat.deleteSession(id);
134
+ }}
135
+ >
136
+ Delete
137
+ </Button>
138
+ </div>
139
+ ),
140
+ });
141
+ };
142
+
107
143
  return (
108
144
  <div className="chat-shell">
109
145
  <ChatTopBar
@@ -116,7 +152,9 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
116
152
  onOpenOverview={() => setActiveTab?.('overview')}
117
153
  />
118
154
  <div className="chat-body">
119
- <aside className={`chat-sessions ${!sessionsOpen ? 'chat-sessions-hidden' : ''}`}>
155
+ <aside
156
+ className={`chat-sessions ${!sessionsOpen ? 'chat-sessions-hidden' : ''}`}
157
+ >
120
158
  <SessionList
121
159
  sessions={chat.sessions}
122
160
  opencodeSessions={chat.opencodeSessions}
@@ -126,19 +164,30 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
126
164
  onCreateSession={handleCreateSession}
127
165
  onSelectSession={chat.selectBizarSession}
128
166
  onSelectOpencodeSession={(s) => chat.loadOpencodeSession(s.id)}
129
- creating={creating}
167
+ creating={chat.busy.create}
130
168
  />
131
169
  </aside>
132
170
 
133
171
  <main className="chat-main">
134
172
  <ChatThread
135
- messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
173
+ messages={
174
+ chat.activeSource === 'opencode'
175
+ ? chat.opencodeMessages
176
+ : chat.bizarMessages
177
+ }
136
178
  loading={chat.loading}
137
179
  activeProject={snapshot.activeProject}
138
- sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
180
+ sessionId={
181
+ chat.activeSource === 'opencode'
182
+ ? chat.activeOpencodeSessionId ?? chat.sessionId
183
+ : chat.sessionId
184
+ }
139
185
  pinned={chat.pinned}
186
+ activeSource={chat.activeSource}
140
187
  onPickSuggestion={(t) => setText(t)}
141
- onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
188
+ onCopy={(m) =>
189
+ chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])
190
+ }
142
191
  onDelete={handleDelete}
143
192
  onTogglePin={chat.togglePin}
144
193
  onRegenerate={chat.onRegenerate}
@@ -151,6 +200,7 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
151
200
  text={text}
152
201
  setText={setText}
153
202
  sending={chat.sending}
203
+ activeSource={chat.activeSource}
154
204
  onSend={handleSend}
155
205
  attachments={attachments}
156
206
  setAttachments={setAttachments}
@@ -161,22 +211,47 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
161
211
  sessionsOpen={sessionsOpen}
162
212
  infoOpen={infoOpen}
163
213
  />
164
- <input ref={fileInputRef} type="file" multiple style={{ display: 'none' }} onChange={onFiles} />
214
+ <input
215
+ ref={fileInputRef}
216
+ type="file"
217
+ multiple
218
+ style={{ display: 'none' }}
219
+ onChange={onFiles}
220
+ />
165
221
  </main>
166
222
 
167
- <aside className={`chat-info ${!infoOpen ? 'chat-info-hidden' : ''}`}>
223
+ <aside
224
+ className={`chat-info ${!infoOpen ? 'chat-info-hidden' : ''}`}
225
+ >
168
226
  <InfoPanel
169
- sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
170
- messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
227
+ sessionId={
228
+ chat.activeSource === 'opencode'
229
+ ? chat.activeOpencodeSessionId ?? chat.sessionId
230
+ : chat.sessionId
231
+ }
232
+ messages={
233
+ chat.activeSource === 'opencode'
234
+ ? chat.opencodeMessages
235
+ : chat.bizarMessages
236
+ }
171
237
  pinned={chat.pinned}
172
238
  agent={agent}
173
239
  model={model}
174
240
  agents={snapshot.agents || []}
175
241
  mcps={snapshot.mcps || []}
176
242
  allCommands={allCommands}
243
+ activeSource={chat.activeSource}
244
+ onDelete={() => {
245
+ const id =
246
+ chat.activeSource === 'opencode'
247
+ ? chat.activeOpencodeSessionId
248
+ : chat.sessionId;
249
+ if (!id) return;
250
+ handleDeleteSession(id, id);
251
+ }}
177
252
  />
178
253
  </aside>
179
254
  </div>
180
255
  </div>
181
256
  );
182
- }
257
+ }
@@ -1598,4 +1598,138 @@
1598
1598
  font-size: 0.8125rem; color: var(--text-muted); margin: 0;
1599
1599
  max-width: 320px;
1600
1600
  }
1601
- .chat-empty-state-action { margin-top: 0.5rem; }
1601
+ .chat-empty-state-action { margin-top: 0.5rem; }
1602
+
1603
+ /* ============================================================
1604
+ v4.2.5 — CHAT OVERHAUL: source badge, info-actions, error
1605
+ row, and source-tinted composer. These are additive — they
1606
+ don't override any of the legacy rules above.
1607
+ ============================================================ */
1608
+
1609
+ /* ---- source badge (opencode / bizar chat) ---- */
1610
+ .chat-source-badge {
1611
+ display: inline-flex; align-items: center;
1612
+ padding: 2px 8px;
1613
+ border-radius: var(--radius-pill);
1614
+ font: 10px/1 var(--font-mono);
1615
+ letter-spacing: 0.04em;
1616
+ text-transform: uppercase;
1617
+ border: 1px solid transparent;
1618
+ }
1619
+ .chat-source-opencode {
1620
+ background: rgba(139, 92, 246, 0.12);
1621
+ color: var(--accent);
1622
+ border-color: rgba(139, 92, 246, 0.4);
1623
+ }
1624
+ .chat-source-bizar {
1625
+ background: var(--bg-2);
1626
+ color: var(--text-muted);
1627
+ border-color: var(--border);
1628
+ }
1629
+ .chat-source-none {
1630
+ background: var(--bg-2);
1631
+ color: var(--text-muted);
1632
+ border-color: var(--border);
1633
+ opacity: 0.7;
1634
+ }
1635
+
1636
+ /* ---- thread-head title row (so badge sits inline) ---- */
1637
+ .chat-thread-title-row {
1638
+ display: flex; align-items: center; gap: 8px;
1639
+ flex-wrap: wrap;
1640
+ }
1641
+
1642
+ /* ---- thread error row (SSE/upstream failures) ---- */
1643
+ .chat-thread-error {
1644
+ margin-top: 4px;
1645
+ padding: 4px 10px;
1646
+ background: rgba(248, 81, 73, 0.08);
1647
+ border: 1px solid rgba(248, 81, 73, 0.4);
1648
+ border-radius: var(--radius-sm);
1649
+ font: 11px/1.3 var(--font-mono);
1650
+ color: var(--error);
1651
+ }
1652
+
1653
+ /* ---- info-panel section heads + actions ---- */
1654
+ .chat-info-section-head {
1655
+ display: flex; align-items: center;
1656
+ margin-bottom: 8px;
1657
+ }
1658
+ .chat-info-value {
1659
+ font: 13px/1.4 var(--font-sans);
1660
+ color: var(--text);
1661
+ }
1662
+ .chat-info-actions {
1663
+ display: flex; flex-wrap: wrap; gap: 6px;
1664
+ margin-top: 8px;
1665
+ }
1666
+ .chat-info-actions .btn {
1667
+ font: 11px/1 var(--font-sans);
1668
+ padding: 6px 10px;
1669
+ border: 1px solid var(--border);
1670
+ background: var(--bg);
1671
+ color: var(--text);
1672
+ border-radius: var(--radius-sm);
1673
+ cursor: pointer;
1674
+ display: inline-flex; align-items: center; gap: 4px;
1675
+ transition: background var(--motion-fast) var(--motion-ease);
1676
+ }
1677
+ .chat-info-actions .btn:hover:not(:disabled) {
1678
+ background: var(--bg-1);
1679
+ }
1680
+ .chat-info-actions .btn:disabled { opacity: 0.5; cursor: not-allowed; }
1681
+ .chat-info-actions .btn-danger { color: var(--error); border-color: rgba(248, 81, 73, 0.4); }
1682
+ .chat-info-actions .btn-danger:hover:not(:disabled) {
1683
+ background: rgba(248, 81, 73, 0.08);
1684
+ }
1685
+
1686
+ /* ---- btn-ghost tokens (also defined in main.css but we re-stamp here for safety) ---- */
1687
+ .btn-ghost {
1688
+ display: inline-flex; align-items: center; gap: 4px;
1689
+ padding: 4px 8px;
1690
+ background: transparent;
1691
+ border: 1px solid transparent;
1692
+ border-radius: var(--radius-sm);
1693
+ color: var(--text-muted);
1694
+ font: 11px/1 var(--font-mono);
1695
+ cursor: pointer;
1696
+ transition: background var(--motion-fast) var(--motion-ease),
1697
+ color var(--motion-fast) var(--motion-ease);
1698
+ }
1699
+ .btn-ghost:hover:not(:disabled) { background: var(--bg-1); color: var(--text); }
1700
+ .btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; }
1701
+ .btn-ghost.btn-danger { color: var(--error); }
1702
+ .btn-ghost.btn-sm { padding: 4px 6px; font-size: 11px; }
1703
+
1704
+ /* ---- composer: source-tinted pill border + hint line ---- */
1705
+ .chat-composer-source-opencode .chat-composer-pill {
1706
+ border-color: rgba(139, 92, 246, 0.35);
1707
+ }
1708
+ .chat-composer-source-opencode .chat-composer-pill:focus-within {
1709
+ border-color: var(--accent);
1710
+ }
1711
+ .chat-composer-source-opencode::before {
1712
+ content: '';
1713
+ position: absolute;
1714
+ top: 0; left: 0; right: 0;
1715
+ height: 1px;
1716
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
1717
+ opacity: 0.6;
1718
+ }
1719
+ .chat-composer-wrap {
1720
+ position: relative;
1721
+ }
1722
+ .chat-composer-source-hint {
1723
+ margin-left: auto;
1724
+ font: 10px/1 var(--font-mono);
1725
+ letter-spacing: 0.04em;
1726
+ text-transform: uppercase;
1727
+ color: var(--text-muted);
1728
+ opacity: 0.8;
1729
+ }
1730
+
1731
+ /* ---- legacy composer pointer-events fix ---- */
1732
+ .chat-composer-wrap.legacy {
1733
+ pointer-events: none;
1734
+ }
1735
+ .chat-composer-wrap.legacy > * { pointer-events: auto; }
@@ -166,6 +166,18 @@ pre,
166
166
  --space-10: 40px;
167
167
  --space-12: 48px;
168
168
 
169
+ /* v4.6.0 — Semantic spacing aliases. These mirror the existing
170
+ --space-* numeric scale so existing rules stay untouched, and
171
+ new code can pick the alias that fits the intent. Use whichever
172
+ reads clearer at the call site; they resolve to the same pixel
173
+ values. Goal: kill the "is this 12 or 16?" guessing in
174
+ view-level padding. */
175
+ --spacing-xs: var(--space-1); /* 4px — micro gap, icon-to-text */
176
+ --spacing-sm: var(--space-2); /* 8px — tight stack, between rows */
177
+ --spacing-md: var(--space-3); /* 12px — default card padding */
178
+ --spacing-lg: var(--space-4); /* 16px — section padding */
179
+ --spacing-xl: var(--space-6); /* 24px — page-level padding */
180
+
169
181
  /* Radius */
170
182
  --radius-sm: 4px;
171
183
  --radius: 8px;
@@ -417,6 +429,34 @@ pre,
417
429
  font-size: 12px;
418
430
  }
419
431
 
432
+ /* v3.6.2 — Token-entry form on boot-error screen */
433
+ .token-entry-form {
434
+ margin: var(--space-4) 0;
435
+ display: flex;
436
+ flex-direction: column;
437
+ gap: var(--space-2);
438
+ }
439
+ .token-entry-row {
440
+ display: flex;
441
+ gap: var(--space-2);
442
+ align-items: center;
443
+ }
444
+ .token-entry-row .input {
445
+ flex: 1;
446
+ min-width: 0;
447
+ }
448
+ .token-entry-hint {
449
+ margin: 0;
450
+ font-size: 12px;
451
+ color: var(--text-dim);
452
+ }
453
+ .token-entry-hint code {
454
+ background: var(--bg);
455
+ padding: 1px 4px;
456
+ border-radius: var(--radius-sm);
457
+ font-size: 11px;
458
+ }
459
+
420
460
  /* ─── Topbar ───────────────────────────────────────────────────────── */
421
461
 
422
462
  .topbar {
@@ -5581,6 +5621,12 @@ html[data-compact-mode="true"] {
5581
5621
  --space-4: 12px;
5582
5622
  --space-5: 16px;
5583
5623
  --space-6: 20px;
5624
+ /* Spacing aliases track the scaled numeric scale. */
5625
+ --spacing-xs: var(--space-1);
5626
+ --spacing-sm: var(--space-2);
5627
+ --spacing-md: var(--space-3);
5628
+ --spacing-lg: var(--space-4);
5629
+ --spacing-xl: var(--space-6);
5584
5630
  }
5585
5631
  html[data-compact-mode="true"] .kanban-col-header { padding: 4px; }
5586
5632
  html[data-compact-mode="true"] .task-card { padding: 6px; }