bloby-bot 0.60.0 → 0.61.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.
- package/dist-bloby/assets/{bloby-8GjzRxjC.js → bloby-DO7g-v11.js} +4 -4
- package/dist-bloby/assets/globals-CF0bs396.css +2 -0
- package/dist-bloby/assets/{globals-D-b6XZqk.js → globals-CwR3dDCz.js} +2 -2
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-DrKKm93B.js → highlighted-body-OFNGDK62-C2Wmb17B.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-CILe07ZG.js +1 -0
- package/dist-bloby/assets/{onboard-DJNuzfZA.js → onboard-DcGLkITd.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +4 -3
- package/shared/config.ts +25 -0
- package/supervisor/channels/manager.ts +112 -12
- package/supervisor/channels/telegram.ts +361 -0
- package/supervisor/channels/types.ts +5 -1
- package/supervisor/channels/whatsapp.ts +4 -5
- package/supervisor/chat/OnboardWizard.tsx +163 -110
- package/supervisor/harnesses/claude.ts +7 -0
- package/supervisor/harnesses/pi/index.ts +1 -1
- package/supervisor/harnesses/pi/tools/path-safety.ts +8 -1
- package/supervisor/index.ts +334 -7
- package/supervisor/workspace-guard.js +3 -3
- package/worker/prompts/bloby-system-prompt-codex.txt +2 -2
- package/worker/prompts/bloby-system-prompt-pi.txt +2 -2
- package/worker/prompts/bloby-system-prompt.txt +2 -2
- package/workspace/skills/telegram/.claude-plugin/plugin.json +6 -0
- package/workspace/skills/telegram/SKILL.md +230 -0
- package/workspace/skills/telegram/skill.json +15 -0
- package/dist-bloby/assets/globals-eJ7lScsq.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-CxqocSKs.js +0 -1
|
@@ -518,11 +518,10 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
// Skip if no text AND no images
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (!rawText && images.length > 0) {
|
|
521
|
+
// Skip if no text AND no images; otherwise default text for image-only
|
|
522
|
+
// messages. Collapsing both branches also narrows `rawText` to `string`.
|
|
523
|
+
if (!rawText) {
|
|
524
|
+
if (images.length === 0) continue;
|
|
526
525
|
rawText = '(image)';
|
|
527
526
|
}
|
|
528
527
|
|
|
@@ -60,6 +60,8 @@ const PROVIDERS = [
|
|
|
60
60
|
|
|
61
61
|
const MODELS: Record<string, { id: string; label: string }[]> = {
|
|
62
62
|
anthropic: [
|
|
63
|
+
{ id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M context)' },
|
|
64
|
+
{ id: 'claude-opus-4-8', label: 'Opus 4.8' },
|
|
63
65
|
{ id: 'claude-opus-4-7[1m]', label: 'Opus 4.7 (1M context)' },
|
|
64
66
|
{ id: 'claude-opus-4-7', label: 'Opus 4.7' },
|
|
65
67
|
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6 (1M context)' },
|
|
@@ -87,7 +89,7 @@ const HANDLES = [
|
|
|
87
89
|
|
|
88
90
|
/* ── Dropdown ── */
|
|
89
91
|
|
|
90
|
-
function ModelDropdown({ models, value, onChange, placeholder = 'Choose a model...' }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void; placeholder?: string }) {
|
|
92
|
+
function ModelDropdown({ models, value, onChange, placeholder = 'Choose a model...', menuMaxHeight = 'max-h-[320px]' }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void; placeholder?: string; menuMaxHeight?: string }) {
|
|
91
93
|
const [open, setOpen] = useState(false);
|
|
92
94
|
const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
93
95
|
const btnRef = useRef<HTMLButtonElement>(null);
|
|
@@ -135,7 +137,7 @@ function ModelDropdown({ models, value, onChange, placeholder = 'Choose a model.
|
|
|
135
137
|
<div
|
|
136
138
|
ref={menuRef}
|
|
137
139
|
style={{ position: 'fixed', top: pos.top, left: pos.left, width: pos.width, zIndex: 1000 }}
|
|
138
|
-
className=
|
|
140
|
+
className={`bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 ${menuMaxHeight} overflow-y-auto`}
|
|
139
141
|
>
|
|
140
142
|
{models.map((m) => (
|
|
141
143
|
<button
|
|
@@ -215,7 +217,7 @@ function GoToMenu({ onJump }: { onJump: (step: number) => void }) {
|
|
|
215
217
|
<div
|
|
216
218
|
ref={menuRef}
|
|
217
219
|
style={{ position: 'fixed', top: pos.top, right: pos.right, zIndex: 1000 }}
|
|
218
|
-
className="bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 min-w-[200px]"
|
|
220
|
+
className="bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 min-w-[200px] max-h-[188px] overflow-y-auto"
|
|
219
221
|
>
|
|
220
222
|
{SETTINGS_SCREENS.map((s) => (
|
|
221
223
|
<button
|
|
@@ -242,6 +244,7 @@ interface EnvCache {
|
|
|
242
244
|
values: Record<string, string>;
|
|
243
245
|
originals: Record<string, string>;
|
|
244
246
|
revealed: string[];
|
|
247
|
+
removed: string[];
|
|
245
248
|
newRows: { id: number; name: string; value: string }[];
|
|
246
249
|
newRowSeq: number;
|
|
247
250
|
saved: boolean;
|
|
@@ -266,6 +269,7 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
266
269
|
const [values, setValues] = useState<Record<string, string>>(cached?.values ?? {});
|
|
267
270
|
const [originals, setOriginals] = useState<Record<string, string>>(cached?.originals ?? {});
|
|
268
271
|
const [revealed, setRevealed] = useState<Set<string>>(new Set(cached?.revealed ?? []));
|
|
272
|
+
const [removed, setRemoved] = useState<Set<string>>(new Set(cached?.removed ?? []));
|
|
269
273
|
const [newRows, setNewRows] = useState<{ id: number; name: string; value: string }[]>(cached?.newRows ?? []);
|
|
270
274
|
const newRowId = useRef(cached?.newRowSeq ?? 0);
|
|
271
275
|
const [saving, setSaving] = useState(false);
|
|
@@ -295,12 +299,14 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
295
299
|
// Skip while loading so a navigate-away mid-fetch leaves the cache empty → refetch on return.
|
|
296
300
|
useEffect(() => {
|
|
297
301
|
if (loading) return;
|
|
298
|
-
cacheRef.current = { groups, values, originals, revealed: Array.from(revealed), newRows, newRowSeq: newRowId.current, saved };
|
|
302
|
+
cacheRef.current = { groups, values, originals, revealed: Array.from(revealed), removed: Array.from(removed), newRows, newRowSeq: newRowId.current, saved };
|
|
299
303
|
});
|
|
300
304
|
|
|
301
305
|
const validNewRows = newRows.filter((r) => r.name.trim() && r.value.trim() && ENV_NAME_RE.test(r.name.trim()));
|
|
302
|
-
const
|
|
303
|
-
|
|
306
|
+
const removedExisting = [...removed].filter((k) => k in originals);
|
|
307
|
+
// An edit to a var that's also marked for removal doesn't count — removal wins.
|
|
308
|
+
const changedExisting = Object.keys(values).filter((k) => values[k] !== originals[k] && !removed.has(k));
|
|
309
|
+
const dirty = changedExisting.length > 0 || validNewRows.length > 0 || removedExisting.length > 0;
|
|
304
310
|
|
|
305
311
|
const toggleReveal = (name: string) => {
|
|
306
312
|
setRevealed((prev) => {
|
|
@@ -310,29 +316,41 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
310
316
|
});
|
|
311
317
|
};
|
|
312
318
|
|
|
319
|
+
const toggleRemoved = (name: string) => {
|
|
320
|
+
setSaved(false);
|
|
321
|
+
setRemoved((prev) => {
|
|
322
|
+
const next = new Set(prev);
|
|
323
|
+
if (next.has(name)) next.delete(name); else next.add(name);
|
|
324
|
+
return next;
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
313
328
|
const handleSave = async () => {
|
|
314
329
|
if (!dirty || saving) return;
|
|
315
330
|
setSaving(true); setSaveError(''); setSaved(false);
|
|
316
331
|
const vars: Record<string, string> = {};
|
|
317
332
|
for (const k of changedExisting) vars[k] = values[k];
|
|
318
333
|
for (const r of validNewRows) vars[r.name.trim()] = r.value.trim();
|
|
334
|
+
const removeList = removedExisting;
|
|
319
335
|
try {
|
|
320
336
|
const res = await authFetch('/api/env', {
|
|
321
337
|
method: 'POST',
|
|
322
338
|
headers: { 'Content-Type': 'application/json' },
|
|
323
|
-
body: JSON.stringify({ vars }),
|
|
339
|
+
body: JSON.stringify({ vars, remove: removeList }),
|
|
324
340
|
});
|
|
325
341
|
if (!res.ok) {
|
|
326
342
|
const d = await res.json().catch(() => ({ error: 'Save failed' }));
|
|
327
343
|
throw new Error(d.error || 'Save failed');
|
|
328
344
|
}
|
|
329
|
-
// Snapshot of values including the just-added rows, so `dirty`
|
|
345
|
+
// Snapshot of values including the just-added rows and minus the removed ones, so `dirty`
|
|
346
|
+
// resets after save.
|
|
330
347
|
const merged = { ...values };
|
|
331
348
|
for (const r of validNewRows) merged[r.name.trim()] = r.value.trim();
|
|
332
|
-
|
|
333
|
-
//
|
|
349
|
+
for (const k of removeList) delete merged[k];
|
|
350
|
+
// Drop removed vars from the displayed groups; surface newly-added vars under a "Custom" group
|
|
351
|
+
// (the writer appends them; on next load they'll re-group under whatever section precedes them).
|
|
334
352
|
setGroups((prev) => {
|
|
335
|
-
|
|
353
|
+
let next = prev.map((g) => ({ ...g, vars: g.vars.filter((v) => !removed.has(v.name)) }));
|
|
336
354
|
const existing = new Set(next.flatMap((g) => g.vars.map((v) => v.name)));
|
|
337
355
|
const toAdd = validNewRows.map((r) => r.name.trim()).filter((nm) => !existing.has(nm));
|
|
338
356
|
if (toAdd.length) {
|
|
@@ -340,11 +358,12 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
340
358
|
if (!custom) { custom = { title: 'Custom', vars: [] }; next.push(custom); }
|
|
341
359
|
for (const nm of toAdd) custom.vars.push({ name: nm, value: merged[nm] });
|
|
342
360
|
}
|
|
343
|
-
return next;
|
|
361
|
+
return next.filter((g) => g.vars.length > 0); // hide groups emptied by removal
|
|
344
362
|
});
|
|
345
363
|
setValues(merged);
|
|
346
364
|
setOriginals(merged);
|
|
347
365
|
setNewRows([]);
|
|
366
|
+
setRemoved(new Set());
|
|
348
367
|
setSaved(true);
|
|
349
368
|
setSaving(false);
|
|
350
369
|
} catch (err: any) {
|
|
@@ -387,34 +406,60 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
387
406
|
</div>
|
|
388
407
|
<div className="space-y-2.5">
|
|
389
408
|
{g.vars.map((v) => {
|
|
390
|
-
const
|
|
409
|
+
const isRemoved = removed.has(v.name);
|
|
410
|
+
const changed = !isRemoved && values[v.name] !== originals[v.name];
|
|
391
411
|
const show = revealed.has(v.name);
|
|
392
412
|
return (
|
|
393
413
|
<div key={v.name}>
|
|
394
414
|
<label className="flex items-center gap-1.5 text-[11px] text-white/40 mb-1">
|
|
395
|
-
<code className=
|
|
415
|
+
<code className={`font-mono ${isRemoved ? 'text-white/30 line-through' : 'text-white/50'}`}>{v.name}</code>
|
|
396
416
|
{changed && <span className="text-[#0069FE] text-[10px] font-medium">• edited</span>}
|
|
417
|
+
{isRemoved && <span className="text-red-400/80 text-[10px] font-medium">• will be removed</span>}
|
|
397
418
|
</label>
|
|
398
|
-
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
{isRemoved ? (
|
|
420
|
+
<div className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-red-500/20 bg-red-500/[0.04]">
|
|
421
|
+
<span className="flex-1 text-[13px] text-white/30 font-mono">••••••••</span>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={() => toggleRemoved(v.name)}
|
|
425
|
+
className="text-[11px] text-white/50 hover:text-white/80 transition-colors"
|
|
426
|
+
>
|
|
427
|
+
Undo
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
) : (
|
|
431
|
+
<div className="flex items-center gap-2">
|
|
432
|
+
<div className="relative flex-1">
|
|
433
|
+
<input
|
|
434
|
+
type={show ? 'text' : 'password'}
|
|
435
|
+
value={values[v.name] ?? ''}
|
|
436
|
+
onChange={(e) => setValues((p) => ({ ...p, [v.name]: e.target.value }))}
|
|
437
|
+
autoComplete="off"
|
|
438
|
+
spellCheck={false}
|
|
439
|
+
data-1p-ignore
|
|
440
|
+
data-lpignore="true"
|
|
441
|
+
className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl pl-4 pr-10 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors"
|
|
442
|
+
/>
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
onClick={() => toggleReveal(v.name)}
|
|
446
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
|
|
447
|
+
aria-label={show ? 'Hide value' : 'Reveal value'}
|
|
448
|
+
>
|
|
449
|
+
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
onClick={() => toggleRemoved(v.name)}
|
|
455
|
+
className="shrink-0 p-2 text-white/25 hover:text-red-400 hover:bg-red-500/[0.06] rounded-lg transition-colors"
|
|
456
|
+
title="Remove variable"
|
|
457
|
+
aria-label="Remove variable"
|
|
458
|
+
>
|
|
459
|
+
<Trash2 className="h-4 w-4" />
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
418
463
|
</div>
|
|
419
464
|
);
|
|
420
465
|
})}
|
|
@@ -489,7 +534,7 @@ function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null>
|
|
|
489
534
|
<div className="mt-4 flex items-start gap-2.5 bg-amber-500/[0.06] border border-amber-500/20 rounded-xl px-4 py-3">
|
|
490
535
|
<TriangleAlert className="h-4 w-4 text-amber-400 mt-0.5 shrink-0" />
|
|
491
536
|
<p className="text-amber-300/80 text-[12px] leading-relaxed">
|
|
492
|
-
|
|
537
|
+
Changes detected. Your workspace backend will be restarted when you save.
|
|
493
538
|
</p>
|
|
494
539
|
</div>
|
|
495
540
|
)}
|
|
@@ -534,7 +579,6 @@ interface CronView {
|
|
|
534
579
|
id: string;
|
|
535
580
|
schedule: string;
|
|
536
581
|
task: string;
|
|
537
|
-
enabled: boolean;
|
|
538
582
|
oneShot?: boolean;
|
|
539
583
|
paused?: boolean;
|
|
540
584
|
hasTaskFile: boolean;
|
|
@@ -740,9 +784,7 @@ function PulseSection({ pulse, original, setPulse, onPulseSaved }: {
|
|
|
740
784
|
/>
|
|
741
785
|
</div>
|
|
742
786
|
</div>
|
|
743
|
-
<p className="text-white/35 text-[12px] mt-2 leading-relaxed">
|
|
744
|
-
No pulses fire between these times (your computer's local time). Spanning midnight is fine — e.g. 22:00 → 07:00.
|
|
745
|
-
</p>
|
|
787
|
+
<p className="text-white/35 text-[12px] mt-2 leading-relaxed">No pulses fire between these times.</p>
|
|
746
788
|
</div>
|
|
747
789
|
|
|
748
790
|
{saved && !dirty && (
|
|
@@ -777,8 +819,14 @@ function CronsSection({ crons, onChanged, onLocalUpdate }: {
|
|
|
777
819
|
const [confirmId, setConfirmId] = useState<string | null>(null);
|
|
778
820
|
const [downloading, setDownloading] = useState<Record<string, boolean>>({});
|
|
779
821
|
const [actionError, setActionError] = useState('');
|
|
822
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set()); // collapsed by default
|
|
780
823
|
|
|
781
824
|
const setRowBusy = (id: string, v: 'pause' | 'delete' | undefined) => setBusy((p) => ({ ...p, [id]: v }));
|
|
825
|
+
const toggleExpanded = (id: string) => setExpanded((prev) => {
|
|
826
|
+
const next = new Set(prev);
|
|
827
|
+
if (next.has(id)) next.delete(id); else next.add(id);
|
|
828
|
+
return next;
|
|
829
|
+
});
|
|
782
830
|
|
|
783
831
|
const handleTogglePause = async (c: CronView) => {
|
|
784
832
|
if (busy[c.id]) return;
|
|
@@ -869,105 +917,94 @@ function CronsSection({ crons, onChanged, onLocalUpdate }: {
|
|
|
869
917
|
) : (
|
|
870
918
|
crons.map((c) => {
|
|
871
919
|
const paused = !!c.paused;
|
|
872
|
-
const disabled = c.enabled === false;
|
|
873
|
-
const parked = paused || disabled;
|
|
874
920
|
const rowBusy = busy[c.id];
|
|
921
|
+
const isOpen = expanded.has(c.id);
|
|
875
922
|
const nextStr = formatNextRun(c.nextRun);
|
|
876
923
|
return (
|
|
877
|
-
<div key={c.id} className={`rounded-xl border
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
</span>
|
|
890
|
-
)}
|
|
891
|
-
{paused && (
|
|
892
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-white/[0.06] border border-white/[0.08] text-white/50 text-[10px] font-medium px-2 py-0.5">
|
|
893
|
-
<Pause className="h-2.5 w-2.5" />Paused
|
|
894
|
-
</span>
|
|
895
|
-
)}
|
|
896
|
-
{disabled && !paused && (
|
|
897
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-white/[0.06] border border-white/[0.08] text-white/50 text-[10px] font-medium px-2 py-0.5">
|
|
898
|
-
Disabled
|
|
899
|
-
</span>
|
|
900
|
-
)}
|
|
901
|
-
</div>
|
|
902
|
-
</div>
|
|
903
|
-
|
|
904
|
-
<div className="mt-1.5 flex items-center gap-1.5 text-[12px] text-white/45">
|
|
905
|
-
<Clock className="h-3 w-3 shrink-0 text-white/30" />
|
|
906
|
-
<span className="truncate">{c.description}</span>
|
|
907
|
-
</div>
|
|
908
|
-
|
|
909
|
-
<div className="mt-1.5">
|
|
910
|
-
{parked ? (
|
|
911
|
-
<span className="text-[11px] text-white/30">{paused ? "Paused — won't run" : "Disabled — won't run"}</span>
|
|
912
|
-
) : nextStr ? (
|
|
913
|
-
<span className="inline-flex items-center gap-1 rounded-md bg-[#0069FE]/[0.08] border border-[#0069FE]/15 text-[#4AA8FF] text-[11px] font-medium px-2 py-0.5">
|
|
914
|
-
Next: {nextStr}
|
|
915
|
-
</span>
|
|
916
|
-
) : (
|
|
917
|
-
<span className="text-[11px] text-white/30">No upcoming run</span>
|
|
918
|
-
)}
|
|
924
|
+
<div key={c.id} className={`rounded-xl border transition-colors ${paused ? 'border-white/[0.05] bg-white/[0.015]' : 'border-white/[0.06] bg-white/[0.02]'}`}>
|
|
925
|
+
{/* Header row — title + actions (always visible) */}
|
|
926
|
+
<div className="flex items-center gap-2 p-2.5">
|
|
927
|
+
<button
|
|
928
|
+
type="button"
|
|
929
|
+
onClick={() => toggleExpanded(c.id)}
|
|
930
|
+
className="flex items-center gap-2.5 flex-1 min-w-0 text-left"
|
|
931
|
+
>
|
|
932
|
+
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${paused ? 'bg-white/[0.04]' : c.oneShot ? 'bg-amber-500/10' : 'bg-[#0069FE]/10'}`}>
|
|
933
|
+
{c.oneShot
|
|
934
|
+
? <Zap className={`h-4 w-4 ${paused ? 'text-white/40' : 'text-amber-400'}`} />
|
|
935
|
+
: <Clock className={`h-4 w-4 ${paused ? 'text-white/40' : 'text-[#0069FE]'}`} />}
|
|
919
936
|
</div>
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
<button
|
|
926
|
-
type="button" onClick={() => handleDownloadTask(c)} disabled={downloading[c.id]}
|
|
927
|
-
className="group inline-flex items-center gap-1.5 rounded-md bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] hover:border-white/[0.12] text-white/50 hover:text-white/80 text-[11px] font-mono px-2 py-1 transition-colors disabled:opacity-50"
|
|
928
|
-
title="Download task instructions"
|
|
929
|
-
>
|
|
930
|
-
{downloading[c.id] ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <FileText className="h-3 w-3 shrink-0" />}
|
|
931
|
-
{c.id}.md
|
|
932
|
-
<Download className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
933
|
-
</button>
|
|
934
|
-
) : (<span />)}
|
|
937
|
+
<span className={`flex-1 min-w-0 truncate text-[13px] ${paused ? 'text-white/45' : 'text-white'}`}>{c.task || '(untitled task)'}</span>
|
|
938
|
+
{paused && (
|
|
939
|
+
<span className="shrink-0 text-[10px] font-medium text-white/40 bg-white/[0.05] rounded-full px-2 py-0.5">Paused</span>
|
|
940
|
+
)}
|
|
941
|
+
</button>
|
|
935
942
|
|
|
936
943
|
{confirmId === c.id ? (
|
|
937
|
-
<div className="flex items-center gap-1
|
|
938
|
-
<span className="text-[11px] text-white/
|
|
944
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
945
|
+
<span className="text-[11px] text-white/45 mr-0.5">Delete?</span>
|
|
939
946
|
<button
|
|
940
947
|
type="button" onClick={() => handleDelete(c)} disabled={rowBusy === 'delete'}
|
|
941
|
-
className="inline-flex items-center gap-1 rounded-md bg-red-500/15 hover:bg-red-500/25 border border-red-500/30 text-red-300 text-[11px] font-medium px-2
|
|
948
|
+
className="inline-flex items-center gap-1 rounded-md bg-red-500/15 hover:bg-red-500/25 border border-red-500/30 text-red-300 text-[11px] font-medium px-2 py-1 transition-colors disabled:opacity-50"
|
|
942
949
|
>
|
|
943
|
-
{rowBusy === 'delete' ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
|
|
944
|
-
Delete
|
|
950
|
+
{rowBusy === 'delete' ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}Delete
|
|
945
951
|
</button>
|
|
946
952
|
<button
|
|
947
953
|
type="button" onClick={() => setConfirmId(null)} disabled={rowBusy === 'delete'}
|
|
948
|
-
className="rounded-md text-white/40 hover:text-white/70 text-[11px] px-
|
|
954
|
+
className="rounded-md text-white/40 hover:text-white/70 text-[11px] px-1.5 py-1 transition-colors"
|
|
949
955
|
>
|
|
950
956
|
Cancel
|
|
951
957
|
</button>
|
|
952
958
|
</div>
|
|
953
959
|
) : (
|
|
954
|
-
<div className="flex items-center gap-
|
|
960
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
955
961
|
<button
|
|
956
962
|
type="button" onClick={() => handleTogglePause(c)} disabled={!!rowBusy}
|
|
957
|
-
|
|
963
|
+
title={paused ? 'Resume' : 'Pause'} aria-label={paused ? 'Resume' : 'Pause'}
|
|
964
|
+
className="p-1.5 rounded-lg text-white/40 hover:text-white/80 hover:bg-white/[0.05] transition-colors disabled:opacity-50"
|
|
958
965
|
>
|
|
959
|
-
{rowBusy === 'pause' ? <LoaderCircle className="h-
|
|
960
|
-
{paused ? 'Resume' : 'Pause'}
|
|
966
|
+
{rowBusy === 'pause' ? <LoaderCircle className="h-4 w-4 animate-spin" /> : paused ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
|
|
961
967
|
</button>
|
|
962
968
|
<button
|
|
963
969
|
type="button" onClick={() => setConfirmId(c.id)} disabled={!!rowBusy}
|
|
964
|
-
|
|
970
|
+
title="Delete" aria-label="Delete"
|
|
971
|
+
className="p-1.5 rounded-lg text-white/30 hover:text-red-400 hover:bg-red-500/[0.06] transition-colors disabled:opacity-50"
|
|
965
972
|
>
|
|
966
|
-
<Trash2 className="h-
|
|
973
|
+
<Trash2 className="h-4 w-4" />
|
|
974
|
+
</button>
|
|
975
|
+
<button
|
|
976
|
+
type="button" onClick={() => toggleExpanded(c.id)}
|
|
977
|
+
title={isOpen ? 'Collapse' : 'Expand'} aria-label={isOpen ? 'Collapse' : 'Expand'}
|
|
978
|
+
className="p-1.5 rounded-lg text-white/30 hover:text-white/70 hover:bg-white/[0.05] transition-colors"
|
|
979
|
+
>
|
|
980
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
967
981
|
</button>
|
|
968
982
|
</div>
|
|
969
983
|
)}
|
|
970
984
|
</div>
|
|
985
|
+
|
|
986
|
+
{/* Expanded detail — schedule, next run, task file */}
|
|
987
|
+
{isOpen && (
|
|
988
|
+
<div className="px-2.5 pb-3 pl-[52px] -mt-0.5">
|
|
989
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] text-white/45">
|
|
990
|
+
<span className="inline-flex items-center gap-1.5"><Clock className="h-3 w-3 text-white/30 shrink-0" />{c.description}</span>
|
|
991
|
+
{c.oneShot && <span className="text-amber-300/70">· One-time</span>}
|
|
992
|
+
{/* paused is already shown by the header pill — don't repeat it here */}
|
|
993
|
+
{!paused && nextStr && <span className="text-[#4AA8FF]">· Next: {nextStr}</span>}
|
|
994
|
+
</div>
|
|
995
|
+
{c.hasTaskFile && (
|
|
996
|
+
<button
|
|
997
|
+
type="button" onClick={() => handleDownloadTask(c)} disabled={downloading[c.id]}
|
|
998
|
+
className="group mt-2 inline-flex items-center gap-1.5 rounded-md bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] hover:border-white/[0.12] text-white/50 hover:text-white/80 text-[11px] font-mono px-2 py-1 transition-colors disabled:opacity-50"
|
|
999
|
+
title="Download task instructions"
|
|
1000
|
+
>
|
|
1001
|
+
{downloading[c.id] ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <FileText className="h-3 w-3 shrink-0" />}
|
|
1002
|
+
{c.id}.md
|
|
1003
|
+
<Download className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
1004
|
+
</button>
|
|
1005
|
+
)}
|
|
1006
|
+
</div>
|
|
1007
|
+
)}
|
|
971
1008
|
</div>
|
|
972
1009
|
);
|
|
973
1010
|
})
|
|
@@ -2087,6 +2124,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2087
2124
|
models={SETTINGS_SCREENS.map((s) => ({ id: String(s.step), label: s.label }))}
|
|
2088
2125
|
value=""
|
|
2089
2126
|
placeholder="Select a setting…"
|
|
2127
|
+
menuMaxHeight="max-h-[188px]"
|
|
2090
2128
|
onChange={(id) => setStep(Number(id))}
|
|
2091
2129
|
/>
|
|
2092
2130
|
</div>
|
|
@@ -3477,6 +3515,21 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
3477
3515
|
{/* ── Auth flow: Anthropic ── */}
|
|
3478
3516
|
{provider === 'anthropic' && (
|
|
3479
3517
|
<div className="space-y-2.5">
|
|
3518
|
+
{/* Anthropic third-party usage policy notice — shown to anyone considering Claude. */}
|
|
3519
|
+
<div className="rounded-xl border border-amber-500/20 bg-amber-500/[0.06] px-4 py-3.5">
|
|
3520
|
+
<div className="flex items-center gap-2 mb-2">
|
|
3521
|
+
<TriangleAlert className="h-4 w-4 text-amber-400 shrink-0" />
|
|
3522
|
+
<h3 className="text-[12.5px] font-semibold text-amber-200/90">Anthropic Third-Party App Policy Update</h3>
|
|
3523
|
+
</div>
|
|
3524
|
+
<div className="space-y-2 text-amber-100/70 text-[12px] leading-relaxed">
|
|
3525
|
+
<p>Starting June 15, 2026, Anthropic will provide a separate Third-Party App credit equal to the amount you pay for your subscription.</p>
|
|
3526
|
+
<p>For example, if you have the Max 5x plan at $100/month, you will receive $100 in credits to use with third-party tools like Bloby.</p>
|
|
3527
|
+
<p>Unfortunately, this is only a fraction of the usage Bloby users had before. We don't control Anthropic's rules, but we do need to follow them.</p>
|
|
3528
|
+
<p>The best alternative right now is a <span className="font-medium text-amber-100/90">ChatGPT subscription</span>, which also offers $100 and $200 plans with much higher usage limits for Bloby.</p>
|
|
3529
|
+
<p>In the short term, Bloby will be optimized for ChatGPT. In the long term, we are building our own model harness so Bloby has more control, more flexibility, and does not depend too heavily on providers that can change their rules at any moment.</p>
|
|
3530
|
+
</div>
|
|
3531
|
+
</div>
|
|
3532
|
+
|
|
3480
3533
|
{isConnected && (
|
|
3481
3534
|
<div className="space-y-2.5">
|
|
3482
3535
|
<div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
|
|
@@ -213,6 +213,11 @@ async function buildConversationOptions(
|
|
|
213
213
|
|
|
214
214
|
return {
|
|
215
215
|
model,
|
|
216
|
+
// Reasoning effort. 'high' = deep reasoning while staying more token-efficient
|
|
217
|
+
// than the CLI's xhigh default on Opus 4.7/4.8 — meaningful given Anthropic's
|
|
218
|
+
// tighter third-party usage limits. Supported on Opus 4.6+/Sonnet 4.6; silently
|
|
219
|
+
// ignored by models without effort support.
|
|
220
|
+
effort: 'high',
|
|
216
221
|
cwd: WORKSPACE_DIR,
|
|
217
222
|
permissionMode: 'bypassPermissions',
|
|
218
223
|
allowDangerouslySkipPermissions: true,
|
|
@@ -648,6 +653,7 @@ export async function startBlobyAgentQuery(
|
|
|
648
653
|
prompt: sdkPrompt,
|
|
649
654
|
options: {
|
|
650
655
|
model,
|
|
656
|
+
effort: 'high', // see buildConversationOptions — token-efficient deep reasoning
|
|
651
657
|
cwd: WORKSPACE_DIR,
|
|
652
658
|
permissionMode: 'bypassPermissions',
|
|
653
659
|
allowDangerouslySkipPermissions: true,
|
|
@@ -762,6 +768,7 @@ export async function runAgentQuery(req: AgentQueryRequest): Promise<AgentQueryR
|
|
|
762
768
|
prompt: req.message,
|
|
763
769
|
options: {
|
|
764
770
|
cwd: WORKSPACE_DIR,
|
|
771
|
+
effort: 'high', // see buildConversationOptions — token-efficient deep reasoning
|
|
765
772
|
permissionMode: 'bypassPermissions',
|
|
766
773
|
allowDangerouslySkipPermissions: true,
|
|
767
774
|
maxTurns,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { log } from '../../../shared/logger.js';
|
|
14
14
|
import { WORKSPACE_DIR } from '../../../shared/paths.js';
|
|
15
|
-
import type { SavedFile } from '
|
|
15
|
+
import type { SavedFile } from '../../file-saver.js';
|
|
16
16
|
import { assembleSystemPrompt } from '../../../worker/prompts/prompt-assembler.js';
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
@@ -12,7 +12,14 @@ export function safeResolve(cwd: string, requested: string): string {
|
|
|
12
12
|
if (!requested || typeof requested !== 'string') {
|
|
13
13
|
throw new Error('Missing file path');
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// Canonicalize cwd (resolves symlinks) so the traversal check below compares real
|
|
16
|
+
// paths. Falls back to a plain resolve when cwd doesn't exist yet (realpath throws).
|
|
17
|
+
let root: string;
|
|
18
|
+
try {
|
|
19
|
+
root = fs.realpathSync.native(cwd);
|
|
20
|
+
} catch {
|
|
21
|
+
root = path.resolve(cwd);
|
|
22
|
+
}
|
|
16
23
|
const abs = path.isAbsolute(requested)
|
|
17
24
|
? path.normalize(requested)
|
|
18
25
|
: path.normalize(path.join(root, requested));
|