@bibobonking/opensol 3.1.5 → 3.2.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/CHANGELOG.md +8 -2
- package/LICENSE +21 -0
- package/README.md +24 -20
- package/desktop/README.md +1 -1
- package/dist/assets/{index-C0yvs6Vl.js → index-BUVnnhRH.js} +1 -1
- package/dist/assets/index-CyxlAgKM.js +271 -0
- package/dist/assets/index-DFeF90S8.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/README.md +15 -68
- package/server/daemon.mjs +6 -1
- package/server/lib/deploy.mjs +5 -10
- package/server/lib/shell.mjs +149 -0
- package/server/lib/shell.test.mjs +72 -0
- package/server/lib/tools.mjs +8 -44
- package/src/agent/adapters/anthropic.ts +2 -3
- package/src/agent/adapters/gemini.ts +2 -3
- package/src/agent/adapters/openai-responses.ts +2 -3
- package/src/agent/adapters/openai.ts +2 -3
- package/src/agent/adapters/shared.ts +3 -0
- package/src/components/ChatView.tsx +17 -54
- package/src/components/Composer.tsx +143 -135
- package/src/components/ContextBar.tsx +2 -2
- package/src/components/HomeDashboard.tsx +7 -4
- package/src/components/MessageItem.tsx +33 -1
- package/src/components/ModelDeck.tsx +5 -27
- package/src/components/SettingsModal.tsx +8 -0
- package/src/components/ToolCallCard.tsx +2 -2
- package/src/components/TrajectoryView.tsx +1 -1
- package/src/components/composer/PlusMenu.tsx +117 -0
- package/src/components/composer/Popover.tsx +109 -0
- package/src/components/composer/WorkspacePicker.tsx +102 -0
- package/src/core/plugins/tools/shell.ts +2 -2
- package/src/core/plugins/tools/subagent.ts +18 -8
- package/src/core/services/agent-loop.ts +45 -43
- package/src/core/services/compaction.ts +289 -22
- package/src/core/services/llm.ts +6 -2
- package/src/core/services/subagents.ts +191 -3
- package/src/core/services/system-prompt.ts +35 -7
- package/src/core/services/tools.ts +2 -1
- package/src/core/types.ts +5 -0
- package/src/lib/compact-plan.test.mjs +62 -0
- package/src/lib/compact-plan.ts +188 -0
- package/src/lib/subagent-types.ts +54 -0
- package/src/lib/tool-meta.ts +40 -0
- package/src/store/settings.ts +3 -0
- package/src/store/ui.ts +12 -0
- package/src/types.ts +26 -0
- package/shared/plugin-builder.test.mjs +0 -43
- package/shared/plugin-package.d.mts +0 -34
- package/shared/plugin-package.mjs +0 -197
- package/shared/plugin-package.test.mjs +0 -94
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -8,33 +8,20 @@ import {
|
|
|
8
8
|
type DragEvent,
|
|
9
9
|
type KeyboardEvent,
|
|
10
10
|
} from 'react'
|
|
11
|
-
import {
|
|
12
|
-
Check,
|
|
13
|
-
ChevronDown,
|
|
14
|
-
FileText,
|
|
15
|
-
FolderOpen,
|
|
16
|
-
Globe2,
|
|
17
|
-
ListChecks,
|
|
18
|
-
Paperclip,
|
|
19
|
-
ShieldCheck,
|
|
20
|
-
Slash,
|
|
21
|
-
Sparkles,
|
|
22
|
-
Square,
|
|
23
|
-
Upload,
|
|
24
|
-
X,
|
|
25
|
-
} from 'lucide-react'
|
|
11
|
+
import { Check, FileText, ListChecks, Sparkles, ShieldCheck, Slash, Square, Upload, X } from 'lucide-react'
|
|
26
12
|
import type { Attachment } from '../types'
|
|
27
|
-
import { useSkills } from '../store/skills'
|
|
28
13
|
import { activeModelId, hasCredentials, useSettings } from '../store/settings'
|
|
29
14
|
import { useActiveModel, useProviderModels } from '../store/models'
|
|
30
15
|
import { useUi } from '../store/ui'
|
|
31
|
-
import {
|
|
16
|
+
import { effortLabel, EFFORT_LABELS } from '../data/providers'
|
|
32
17
|
import { effortOptions } from '../lib/reasoning'
|
|
33
18
|
import { PERMISSION_MODES, permissionLabel, permissionMode, permissionTools } from '../lib/permissions'
|
|
34
|
-
import { useClickOutside } from '../lib/hooks'
|
|
35
19
|
import { cn, formatBytes, readFileAsDataUrl, readFileAsText } from '../lib/utils'
|
|
36
20
|
import { ModelDeck, ModelDeckTrigger } from './ModelDeck'
|
|
37
21
|
import { PixleSol } from './PixleSol'
|
|
22
|
+
import { Popover, MenuLabel, TextButton } from './composer/Popover'
|
|
23
|
+
import { PlusMenu } from './composer/PlusMenu'
|
|
24
|
+
import { WorkspacePicker } from './composer/WorkspacePicker'
|
|
38
25
|
import type { PixleMood } from '../lib/pixlesol'
|
|
39
26
|
|
|
40
27
|
const TEXT_EXTENSIONS = /\.(txt|md|markdown|ts|tsx|js|jsx|mjs|cjs|json|css|scss|html|vue|svelte|py|go|rs|rb|java|kt|cs|c|h|cpp|hpp|php|swift|sh|bash|zsh|yml|yaml|toml|ini|sql|csv|xml|gitignore|env)$/i
|
|
@@ -50,6 +37,7 @@ const SLASH_COMMANDS = [
|
|
|
50
37
|
{ name: 'workspace', usage: '/workspace', description: 'Open the folder panel', takesArgs: false },
|
|
51
38
|
{ name: 'deploy', usage: '/deploy', description: 'Deploy a project folder', takesArgs: false },
|
|
52
39
|
{ name: 'settings', usage: '/settings', description: 'Open settings', takesArgs: false },
|
|
40
|
+
{ name: 'compact', usage: '/compact [focus]', description: 'Summarise older context to free up space', takesArgs: true },
|
|
53
41
|
{ name: 'clear', usage: '/clear', description: 'Clear this conversation', takesArgs: false },
|
|
54
42
|
]
|
|
55
43
|
|
|
@@ -90,22 +78,19 @@ export function Composer({
|
|
|
90
78
|
const update = useSettings((state) => state.update)
|
|
91
79
|
const setMode = useSettings((state) => state.setMode)
|
|
92
80
|
const notify = useUi((state) => state.notify)
|
|
93
|
-
const openSettings = useUi((state) => state.openSettings)
|
|
94
81
|
const focusToken = useUi((state) => state.composerFocus)
|
|
95
|
-
const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
|
|
96
|
-
const workspace = useWorkspace((state) => state.info)
|
|
97
82
|
const [attachments, setAttachments] = useState<Attachment[]>([])
|
|
98
83
|
const [focused, setFocused] = useState(false)
|
|
99
84
|
const [dragging, setDragging] = useState(false)
|
|
100
85
|
const [pickerOpen, setPickerOpen] = useState(false)
|
|
101
86
|
const [permissionOpen, setPermissionOpen] = useState(false)
|
|
87
|
+
const [effortOpen, setEffortOpen] = useState(false)
|
|
88
|
+
const [modeOpen, setModeOpen] = useState(false)
|
|
102
89
|
const [submitting, setSubmitting] = useState(false)
|
|
103
90
|
const [commandIndex, setCommandIndex] = useState(0)
|
|
104
91
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
105
92
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
106
93
|
const modelTriggerRef = useRef<HTMLButtonElement>(null)
|
|
107
|
-
const permissionRef = useRef<HTMLDivElement>(null)
|
|
108
|
-
const permissionTriggerRef = useRef<HTMLButtonElement>(null)
|
|
109
94
|
|
|
110
95
|
useEffect(() => {
|
|
111
96
|
const onKey = (event: globalThis.KeyboardEvent) => {
|
|
@@ -118,14 +103,11 @@ export function Composer({
|
|
|
118
103
|
return () => window.removeEventListener('keydown', onKey)
|
|
119
104
|
}, [])
|
|
120
105
|
|
|
121
|
-
useClickOutside(permissionRef, () => setPermissionOpen(false), permissionOpen, permissionTriggerRef)
|
|
122
|
-
|
|
123
106
|
const modelId = activeModelId(settings)
|
|
124
107
|
const providerModels = useProviderModels(settings.provider)
|
|
125
108
|
const activeModel = useActiveModel()
|
|
126
109
|
const connected = hasCredentials(settings)
|
|
127
110
|
const efforts = effortOptions(activeModel)
|
|
128
|
-
const activeSkills = useSkills((state) => state.skills.filter((skill) => skill.enabled).length)
|
|
129
111
|
const permission = permissionMode(settings.tools)
|
|
130
112
|
const modelLabel = !connected
|
|
131
113
|
? 'No provider'
|
|
@@ -153,7 +135,7 @@ export function Composer({
|
|
|
153
135
|
const node = textareaRef.current
|
|
154
136
|
if (!node) return
|
|
155
137
|
node.style.height = 'auto'
|
|
156
|
-
node.style.height = `${Math.min(Math.max(node.scrollHeight,
|
|
138
|
+
node.style.height = `${Math.min(Math.max(node.scrollHeight, 36), 220)}px`
|
|
157
139
|
}, [value])
|
|
158
140
|
|
|
159
141
|
useEffect(() => {
|
|
@@ -377,49 +359,8 @@ export function Composer({
|
|
|
377
359
|
</div>
|
|
378
360
|
)}
|
|
379
361
|
|
|
380
|
-
<div className="mb-
|
|
381
|
-
<
|
|
382
|
-
type="button"
|
|
383
|
-
onClick={() => setWorkspaceOpen(true)}
|
|
384
|
-
className="chip h-7 max-w-[220px] gap-1.5 px-2.5 text-[11px]"
|
|
385
|
-
title={workspace.available ? workspace.root : 'Choose a project folder'}
|
|
386
|
-
>
|
|
387
|
-
<FolderOpen size={12} className="shrink-0 text-faint" />
|
|
388
|
-
<span className="truncate">{workspace.available ? workspace.name : 'No folder'}</span>
|
|
389
|
-
</button>
|
|
390
|
-
<button
|
|
391
|
-
type="button"
|
|
392
|
-
onClick={() => setMode(settings.mode === 'plan' ? 'build' : 'plan')}
|
|
393
|
-
className="chip h-7 gap-1.5 px-2.5 text-[11px]"
|
|
394
|
-
title={settings.mode === 'plan' ? 'Switch to Build mode' : 'Switch to Plan mode (read-only)'}
|
|
395
|
-
>
|
|
396
|
-
{settings.mode === 'plan' ? (
|
|
397
|
-
<ListChecks size={12} className="text-faint" />
|
|
398
|
-
) : (
|
|
399
|
-
<Sparkles size={12} className="text-faint" />
|
|
400
|
-
)}
|
|
401
|
-
{settings.mode === 'plan' ? 'Plan mode' : 'Build mode'}
|
|
402
|
-
</button>
|
|
403
|
-
<button
|
|
404
|
-
type="button"
|
|
405
|
-
onClick={() => update({ webSearchEnabled: !settings.webSearchEnabled })}
|
|
406
|
-
className={cn('chip h-7 gap-1.5 px-2.5 text-[11px]', settings.webSearchEnabled && 'chip-active')}
|
|
407
|
-
title="Allow OpenSol to search the web and read pages"
|
|
408
|
-
aria-pressed={settings.webSearchEnabled}
|
|
409
|
-
>
|
|
410
|
-
<Globe2 size={12} />
|
|
411
|
-
Search
|
|
412
|
-
</button>
|
|
413
|
-
{activeSkills > 0 && (
|
|
414
|
-
<button
|
|
415
|
-
type="button"
|
|
416
|
-
onClick={() => openSettings('skills')}
|
|
417
|
-
className="chip h-7 gap-1 px-2.5 text-[11px]"
|
|
418
|
-
title="Manage loaded skills"
|
|
419
|
-
>
|
|
420
|
-
{activeSkills} skill{activeSkills === 1 ? '' : 's'}
|
|
421
|
-
</button>
|
|
422
|
-
)}
|
|
362
|
+
<div className="mb-1 flex flex-wrap items-center gap-x-1 px-0.5">
|
|
363
|
+
<WorkspacePicker />
|
|
423
364
|
</div>
|
|
424
365
|
|
|
425
366
|
<div
|
|
@@ -433,17 +374,17 @@ export function Composer({
|
|
|
433
374
|
}}
|
|
434
375
|
onDrop={onDrop}
|
|
435
376
|
className={cn(
|
|
436
|
-
'surface-lift composer-shadow relative rounded-
|
|
377
|
+
'surface-lift composer-shadow relative rounded-xl border transition-all duration-200',
|
|
437
378
|
focused || dragging ? 'border-accent/35' : 'border-line',
|
|
438
379
|
streaming && 'composer-glow border-accent/30',
|
|
439
380
|
dragging && 'ring-4 ring-accent/10',
|
|
440
381
|
)}
|
|
441
382
|
>
|
|
442
383
|
{focused && !dragging && (
|
|
443
|
-
<div className="pointer-events-none absolute inset-0 rounded-
|
|
384
|
+
<div className="pointer-events-none absolute inset-0 rounded-xl ring-4 ring-accent/[0.03]" aria-hidden="true" />
|
|
444
385
|
)}
|
|
445
386
|
|
|
446
|
-
<div className="relative px-3
|
|
387
|
+
<div className="relative px-3 pb-0 pt-2">
|
|
447
388
|
{attachments.length > 0 && (
|
|
448
389
|
<div className="flex flex-wrap gap-2 px-1 pb-2">
|
|
449
390
|
{attachments.map((attachment) =>
|
|
@@ -496,69 +437,138 @@ export function Composer({
|
|
|
496
437
|
placeholder={placeholder}
|
|
497
438
|
rows={1}
|
|
498
439
|
aria-label="Message OpenSol"
|
|
499
|
-
className="block w-full resize-none bg-transparent px-
|
|
500
|
-
style={{ minHeight:
|
|
440
|
+
className="block w-full resize-none bg-transparent px-1.5 py-1 text-[14.5px] leading-[1.55] text-ink outline-none placeholder:text-faint"
|
|
441
|
+
style={{ minHeight: 36, maxHeight: 220 }}
|
|
501
442
|
/>
|
|
502
443
|
</div>
|
|
503
444
|
|
|
504
|
-
<div className="flex items-
|
|
445
|
+
<div className="flex items-center justify-between gap-2 px-1.5 pb-1.5 pt-0.5">
|
|
505
446
|
<div className="flex min-w-0 items-center gap-0.5">
|
|
506
|
-
<
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
<div
|
|
521
|
-
ref={permissionRef}
|
|
522
|
-
className="surface-lift absolute bottom-[calc(100%+8px)] left-0 z-40 w-[290px] overflow-hidden rounded-2xl border border-line p-1.5 shadow-float"
|
|
447
|
+
<PlusMenu onAddFiles={() => fileRef.current?.click()} />
|
|
448
|
+
<Popover
|
|
449
|
+
open={modeOpen}
|
|
450
|
+
onClose={() => setModeOpen(false)}
|
|
451
|
+
width={230}
|
|
452
|
+
trigger={
|
|
453
|
+
<TextButton
|
|
454
|
+
open={modeOpen}
|
|
455
|
+
onClick={() => {
|
|
456
|
+
setPermissionOpen(false)
|
|
457
|
+
setEffortOpen(false)
|
|
458
|
+
setModeOpen((open) => !open)
|
|
459
|
+
}}
|
|
460
|
+
title="Build edits files and runs commands; Plan is read-only"
|
|
523
461
|
>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
{PERMISSION_MODES.map((entry) => (
|
|
528
|
-
<button
|
|
529
|
-
key={entry.value}
|
|
530
|
-
type="button"
|
|
531
|
-
onClick={() => {
|
|
532
|
-
update({ tools: permissionTools(settings.tools, entry.value) })
|
|
533
|
-
setPermissionOpen(false)
|
|
534
|
-
}}
|
|
535
|
-
className="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-raised"
|
|
536
|
-
>
|
|
537
|
-
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
|
538
|
-
{permission === entry.value ? (
|
|
539
|
-
<Check size={12} className="text-accent" />
|
|
540
|
-
) : (
|
|
541
|
-
<span className="h-1.5 w-1.5 rounded-full bg-line" />
|
|
542
|
-
)}
|
|
543
|
-
</span>
|
|
544
|
-
<span className="min-w-0">
|
|
545
|
-
<span className="block text-[11.5px] font-medium text-ink">{entry.label}</span>
|
|
546
|
-
<span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{entry.description}</span>
|
|
547
|
-
</span>
|
|
548
|
-
</button>
|
|
549
|
-
))}
|
|
550
|
-
</div>
|
|
551
|
-
)}
|
|
552
|
-
</div>
|
|
553
|
-
<button
|
|
554
|
-
type="button"
|
|
555
|
-
className="icon-button h-8 w-8"
|
|
556
|
-
onClick={() => fileRef.current?.click()}
|
|
557
|
-
title="Attach images or text files"
|
|
558
|
-
aria-label="Attach files"
|
|
462
|
+
{settings.mode === 'plan' ? 'Plan' : 'Build'}
|
|
463
|
+
</TextButton>
|
|
464
|
+
}
|
|
559
465
|
>
|
|
560
|
-
<
|
|
561
|
-
|
|
466
|
+
<MenuLabel>Mode</MenuLabel>
|
|
467
|
+
{(
|
|
468
|
+
[
|
|
469
|
+
{ value: 'build', label: 'Build', hint: 'Edit files and run commands', icon: Sparkles },
|
|
470
|
+
{ value: 'plan', label: 'Plan', hint: 'Read-only investigation', icon: ListChecks },
|
|
471
|
+
] as const
|
|
472
|
+
).map((entry) => (
|
|
473
|
+
<button
|
|
474
|
+
key={entry.value}
|
|
475
|
+
type="button"
|
|
476
|
+
onClick={() => {
|
|
477
|
+
setMode(entry.value)
|
|
478
|
+
setModeOpen(false)
|
|
479
|
+
}}
|
|
480
|
+
className="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-raised"
|
|
481
|
+
>
|
|
482
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
|
483
|
+
{settings.mode === entry.value ? <Check size={12} className="text-accent" /> : <entry.icon size={12} className="text-faint" />}
|
|
484
|
+
</span>
|
|
485
|
+
<span className="min-w-0">
|
|
486
|
+
<span className="block text-[11.5px] font-medium text-ink">{entry.label}</span>
|
|
487
|
+
<span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{entry.hint}</span>
|
|
488
|
+
</span>
|
|
489
|
+
</button>
|
|
490
|
+
))}
|
|
491
|
+
</Popover>
|
|
492
|
+
<Popover
|
|
493
|
+
open={permissionOpen}
|
|
494
|
+
onClose={() => setPermissionOpen(false)}
|
|
495
|
+
width={290}
|
|
496
|
+
trigger={
|
|
497
|
+
<TextButton
|
|
498
|
+
open={permissionOpen}
|
|
499
|
+
onClick={() => {
|
|
500
|
+
setEffortOpen(false)
|
|
501
|
+
setModeOpen(false)
|
|
502
|
+
setPermissionOpen((open) => !open)
|
|
503
|
+
}}
|
|
504
|
+
title="Choose how much OpenSol can do without asking"
|
|
505
|
+
>
|
|
506
|
+
{permissionLabel(permission)}
|
|
507
|
+
</TextButton>
|
|
508
|
+
}
|
|
509
|
+
>
|
|
510
|
+
<MenuLabel>Permissions</MenuLabel>
|
|
511
|
+
{PERMISSION_MODES.map((entry) => (
|
|
512
|
+
<button
|
|
513
|
+
key={entry.value}
|
|
514
|
+
type="button"
|
|
515
|
+
onClick={() => {
|
|
516
|
+
update({ tools: permissionTools(settings.tools, entry.value) })
|
|
517
|
+
setPermissionOpen(false)
|
|
518
|
+
}}
|
|
519
|
+
className="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-raised"
|
|
520
|
+
>
|
|
521
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
|
522
|
+
{permission === entry.value ? <Check size={12} className="text-accent" /> : <span className="h-1.5 w-1.5 rounded-full bg-line" />}
|
|
523
|
+
</span>
|
|
524
|
+
<span className="min-w-0">
|
|
525
|
+
<span className="block text-[11.5px] font-medium text-ink">{entry.label}</span>
|
|
526
|
+
<span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{entry.description}</span>
|
|
527
|
+
</span>
|
|
528
|
+
</button>
|
|
529
|
+
))}
|
|
530
|
+
</Popover>
|
|
531
|
+
{efforts.length > 1 && (
|
|
532
|
+
<Popover
|
|
533
|
+
open={effortOpen}
|
|
534
|
+
onClose={() => setEffortOpen(false)}
|
|
535
|
+
width={250}
|
|
536
|
+
trigger={
|
|
537
|
+
<TextButton
|
|
538
|
+
open={effortOpen}
|
|
539
|
+
onClick={() => {
|
|
540
|
+
setPermissionOpen(false)
|
|
541
|
+
setModeOpen(false)
|
|
542
|
+
setEffortOpen((open) => !open)
|
|
543
|
+
}}
|
|
544
|
+
title="Reasoning effort for this model"
|
|
545
|
+
>
|
|
546
|
+
{effortLabel(settings.effort)}
|
|
547
|
+
</TextButton>
|
|
548
|
+
}
|
|
549
|
+
>
|
|
550
|
+
<MenuLabel>Effort</MenuLabel>
|
|
551
|
+
{efforts.map((effort) => (
|
|
552
|
+
<button
|
|
553
|
+
key={effort}
|
|
554
|
+
type="button"
|
|
555
|
+
onClick={() => {
|
|
556
|
+
update({ effort })
|
|
557
|
+
setEffortOpen(false)
|
|
558
|
+
}}
|
|
559
|
+
className="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-raised"
|
|
560
|
+
>
|
|
561
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
|
562
|
+
{settings.effort === effort ? <Check size={12} className="text-accent" /> : <span className="h-1.5 w-1.5 rounded-full bg-line" />}
|
|
563
|
+
</span>
|
|
564
|
+
<span className="min-w-0">
|
|
565
|
+
<span className="block text-[11.5px] font-medium text-ink">{EFFORT_LABELS[effort].label}</span>
|
|
566
|
+
<span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{EFFORT_LABELS[effort].description}</span>
|
|
567
|
+
</span>
|
|
568
|
+
</button>
|
|
569
|
+
))}
|
|
570
|
+
</Popover>
|
|
571
|
+
)}
|
|
562
572
|
<input
|
|
563
573
|
ref={fileRef}
|
|
564
574
|
type="file"
|
|
@@ -588,9 +598,7 @@ export function Composer({
|
|
|
588
598
|
open={pickerOpen}
|
|
589
599
|
disabled={!connected}
|
|
590
600
|
onClick={() => setPickerOpen((open) => !open)}
|
|
591
|
-
|
|
592
|
-
effort={settings.effort}
|
|
593
|
-
title={connected ? 'Model deck: choose model and reasoning effort (Ctrl M)' : 'Add a provider key in Settings first'}
|
|
601
|
+
title={connected ? 'Choose a model (Ctrl M)' : 'Add a provider key in Settings first'}
|
|
594
602
|
/>
|
|
595
603
|
{streaming ? (
|
|
596
604
|
<button
|
|
@@ -625,14 +633,14 @@ export function Composer({
|
|
|
625
633
|
</div>
|
|
626
634
|
|
|
627
635
|
{dragging && (
|
|
628
|
-
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-1.5 rounded-
|
|
636
|
+
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-1.5 rounded-xl border-2 border-dashed border-accent/45 bg-panel/95">
|
|
629
637
|
<Upload size={18} className="text-accent" />
|
|
630
638
|
<p className="text-[12px] font-medium text-ink">Drop files to attach</p>
|
|
631
639
|
<p className="text-[10.5px] text-faint">Images up to 5 MB · text up to 512 KB</p>
|
|
632
640
|
</div>
|
|
633
641
|
)}
|
|
634
642
|
|
|
635
|
-
{pixleMood && (
|
|
643
|
+
{pixleMood && settings.showMascot && (
|
|
636
644
|
<PixleSol
|
|
637
645
|
mood={pixleMood}
|
|
638
646
|
size={44}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useMemo } from 'react'
|
|
10
10
|
import type { ModelInfo, Message } from '../types'
|
|
11
11
|
import { getContextUsage, AUTO_COMPACT_THRESHOLD } from '../lib/context'
|
|
12
|
-
import {
|
|
12
|
+
import { useRuntime } from '../store/ui'
|
|
13
13
|
import { formatCredits, sumCost } from '../lib/credits'
|
|
14
14
|
import { cn } from '../lib/utils'
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ function formatTokens(n: number): string {
|
|
|
28
28
|
export function ContextBar({ messages, model, sessionId }: ContextBarProps) {
|
|
29
29
|
const usage = useMemo(() => getContextUsage(messages, model), [messages, model])
|
|
30
30
|
const spent = useMemo(() => sumCost(messages), [messages])
|
|
31
|
-
const compacting = sessionId ?
|
|
31
|
+
const compacting = useRuntime((state) => (sessionId ? Boolean(state.compacting[sessionId]) : false))
|
|
32
32
|
|
|
33
33
|
if (messages.length === 0) return null
|
|
34
34
|
|
|
@@ -44,15 +44,18 @@ function Stat({ icon: Icon, label, value }: { icon: typeof Activity; label: stri
|
|
|
44
44
|
|
|
45
45
|
export function HomeGreeting() {
|
|
46
46
|
const daemon = useSettings((state) => state.daemon)
|
|
47
|
+
const showMascot = useSettings((state) => state.showMascot)
|
|
47
48
|
const workspace = useWorkspace((state) => state.info)
|
|
48
49
|
const home = daemon?.home ?? ''
|
|
49
50
|
const name = home.split(/[\\/]/).filter(Boolean).pop() || workspace.name || 'there'
|
|
50
51
|
return (
|
|
51
52
|
<div className="flex flex-col items-center gap-4 text-center">
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
{showMascot && (
|
|
54
|
+
<div className="relative">
|
|
55
|
+
<span className="absolute inset-0 -z-10 m-auto h-16 w-16 rounded-full bg-accent/20 blur-2xl" aria-hidden="true" />
|
|
56
|
+
<PixleSol mood="wave" size={60} />
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
56
59
|
<h1 className="text-[28px] font-semibold leading-tight tracking-[-0.035em] text-ink sm:text-[34px]">
|
|
57
60
|
What should we build, <span className="text-solar">{name}</span>?
|
|
58
61
|
</h1>
|
|
@@ -18,7 +18,7 @@ import { regenerateLastTurn, runAgent } from '../agent'
|
|
|
18
18
|
import { restoreCheckpoint } from '../lib/daemon'
|
|
19
19
|
import { useWorkspace } from '../store/workspace'
|
|
20
20
|
import { createId, useChats } from '../store/chats'
|
|
21
|
-
import { toolLabel } from '../
|
|
21
|
+
import { toolLabel } from '../lib/tool-meta'
|
|
22
22
|
import { formatBytes, cn } from '../lib/utils'
|
|
23
23
|
import { Markdown } from './Markdown'
|
|
24
24
|
import { ToolCallCard, diffForTool } from './ToolCallCard'
|
|
@@ -375,6 +375,37 @@ export function UsedGroup({ tools, streaming = false }: { tools: ToolMessage[];
|
|
|
375
375
|
return <TaskCompletedBox tools={tools} streaming={streaming} />
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
const COMPACTION_PREFIX = /^\[Summary of the earlier conversation[^\n]*\]\n\n/
|
|
379
|
+
|
|
380
|
+
/** The summary that replaced older messages: a quiet divider that opens to the full text. */
|
|
381
|
+
function CompactionNote({ message }: { message: AssistantMessage }) {
|
|
382
|
+
const [open, setOpen] = useState(false)
|
|
383
|
+
const meta = message.compaction
|
|
384
|
+
if (!meta) return null
|
|
385
|
+
return (
|
|
386
|
+
<div className="my-1">
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
onClick={() => setOpen((value) => !value)}
|
|
390
|
+
className="group/compact flex w-full items-center gap-3 text-[11.5px] text-faint"
|
|
391
|
+
aria-expanded={open}
|
|
392
|
+
>
|
|
393
|
+
<span className="h-px flex-1 bg-line" />
|
|
394
|
+
<span className="shrink-0 transition-colors group-hover/compact:text-ink">
|
|
395
|
+
Context compacted · {meta.turns} turn{meta.turns === 1 ? '' : 's'}, {meta.toolCalls} tool call{meta.toolCalls === 1 ? '' : 's'} summarised
|
|
396
|
+
{meta.fallback ? ' · without the model' : ''}
|
|
397
|
+
</span>
|
|
398
|
+
<span className="h-px flex-1 bg-line" />
|
|
399
|
+
</button>
|
|
400
|
+
{open && (
|
|
401
|
+
<div className="animate-fade mt-2 rounded-xl border border-line bg-raised/40 px-4 py-3">
|
|
402
|
+
<Markdown content={message.content.replace(COMPACTION_PREFIX, '')} />
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
378
409
|
export function AssistantBlock({
|
|
379
410
|
message,
|
|
380
411
|
sessionId,
|
|
@@ -402,6 +433,7 @@ export function AssistantBlock({
|
|
|
402
433
|
// Thoughts stay closed until the person opens them, even while the model is still thinking.
|
|
403
434
|
const [reasoningOpen, setReasoningOpen] = useState(false)
|
|
404
435
|
const hasBody = Boolean(message.content.trim() || message.reasoning.trim() || message.error)
|
|
436
|
+
if (message.compaction) return <CompactionNote message={message} />
|
|
405
437
|
const modelName =
|
|
406
438
|
providerModels?.find((model) => model.id === message.model)?.name ?? message.model ?? ''
|
|
407
439
|
const needsConfig = Boolean(message.error && /api key|base url|settings|credential|403|401/i.test(message.error))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEvent } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { AlertCircle, Brain, Check, Eye, KeyRound, Plus, RefreshCw, Search, Settings2, X, Zap } from 'lucide-react'
|
|
3
|
+
import { AlertCircle, Brain, Check, ChevronDown, Eye, KeyRound, Plus, RefreshCw, Search, Settings2, X, Zap } from 'lucide-react'
|
|
4
4
|
import { PROVIDERS, PROVIDER_MAP, EFFORT_LABELS, effortLabel } from '../data/providers'
|
|
5
5
|
import type { Effort, ModelInfo, ProviderId } from '../types'
|
|
6
6
|
import { activeModelId, providerHasCredentials, useSettings } from '../store/settings'
|
|
@@ -33,25 +33,6 @@ function monogram(name: string) {
|
|
|
33
33
|
return (words.length > 1 ? words[0][0] + words[1][0] : name.slice(0, 2)).toUpperCase()
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** Signal-bar style effort meter used on the composer trigger. */
|
|
37
|
-
export function EffortBars({ efforts, value, className }: { efforts: Effort[]; value: Effort; className?: string }) {
|
|
38
|
-
const index = Math.max(0, efforts.indexOf(value))
|
|
39
|
-
return (
|
|
40
|
-
<span className={cn('flex h-3.5 items-end gap-[2px]', className)} aria-hidden>
|
|
41
|
-
{efforts.map((effort, i) => (
|
|
42
|
-
<span
|
|
43
|
-
key={effort}
|
|
44
|
-
style={{ height: `${38 + (i / Math.max(1, efforts.length - 1)) * 62}%` }}
|
|
45
|
-
className={cn(
|
|
46
|
-
'w-[3px] rounded-[1px] transition-all duration-300 ease-spring',
|
|
47
|
-
i <= index ? 'bg-accent' : 'bg-line',
|
|
48
|
-
)}
|
|
49
|
-
/>
|
|
50
|
-
))}
|
|
51
|
-
</span>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
36
|
function EffortThrottle({
|
|
56
37
|
efforts,
|
|
57
38
|
value,
|
|
@@ -628,14 +609,13 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
|
|
|
628
609
|
}
|
|
629
610
|
|
|
630
611
|
/** Composer trigger: model name with live effort bars, no dropdown chevron. */
|
|
612
|
+
/** Plain-text model name for the composer; opens the deck. */
|
|
631
613
|
export function ModelDeckTrigger({
|
|
632
614
|
label,
|
|
633
615
|
open,
|
|
634
616
|
disabled,
|
|
635
617
|
onClick,
|
|
636
618
|
triggerRef,
|
|
637
|
-
efforts,
|
|
638
|
-
effort,
|
|
639
619
|
title,
|
|
640
620
|
}: {
|
|
641
621
|
label: string
|
|
@@ -643,8 +623,6 @@ export function ModelDeckTrigger({
|
|
|
643
623
|
disabled: boolean
|
|
644
624
|
onClick: () => void
|
|
645
625
|
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
646
|
-
efforts: Effort[]
|
|
647
|
-
effort: Effort
|
|
648
626
|
title: string
|
|
649
627
|
}) {
|
|
650
628
|
return (
|
|
@@ -657,12 +635,12 @@ export function ModelDeckTrigger({
|
|
|
657
635
|
aria-expanded={open}
|
|
658
636
|
title={title}
|
|
659
637
|
className={cn(
|
|
660
|
-
'deck-trigger
|
|
661
|
-
open && '
|
|
638
|
+
'deck-trigger flex h-8 max-w-[180px] items-center gap-1 rounded-lg px-2 text-[12px] font-medium text-muted transition-colors hover:text-ink disabled:cursor-not-allowed disabled:opacity-45 sm:max-w-[260px]',
|
|
639
|
+
open && 'text-ink',
|
|
662
640
|
)}
|
|
663
641
|
>
|
|
664
642
|
<span className="truncate">{label}</span>
|
|
665
|
-
|
|
643
|
+
<ChevronDown size={11} className={cn('shrink-0 text-faint transition-transform', open && 'rotate-180')} />
|
|
666
644
|
</button>
|
|
667
645
|
)
|
|
668
646
|
}
|
|
@@ -1602,6 +1602,14 @@ function GeneralTab() {
|
|
|
1602
1602
|
/>
|
|
1603
1603
|
</div>
|
|
1604
1604
|
|
|
1605
|
+
<div className="settings-row">
|
|
1606
|
+
<div className="min-w-0">
|
|
1607
|
+
<p className="settings-label">Show mascot</p>
|
|
1608
|
+
<p className="settings-hint">The pixel mascot on the home screen and above the message box. Off by default.</p>
|
|
1609
|
+
</div>
|
|
1610
|
+
<Switch checked={settings.showMascot} onChange={(showMascot) => update({ showMascot })} label="Show mascot" />
|
|
1611
|
+
</div>
|
|
1612
|
+
|
|
1605
1613
|
<div className="settings-row">
|
|
1606
1614
|
<div className="min-w-0">
|
|
1607
1615
|
<p className="settings-label">Show token estimates</p>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import { ArrowRight } from 'lucide-react'
|
|
3
3
|
import type { FileDiff, ToolMessage, ToolName } from '../types'
|
|
4
|
-
import { toolLabel, toolSummary } from '../
|
|
5
|
-
import { resolveApproval } from '../
|
|
4
|
+
import { toolLabel, toolSummary } from '../lib/tool-meta'
|
|
5
|
+
import { resolveApproval } from '../core/services/approval'
|
|
6
6
|
import { useUi } from '../store/ui'
|
|
7
7
|
import { cn } from '../lib/utils'
|
|
8
8
|
import { diffLines, diffStats } from '../lib/diff'
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
Wrench,
|
|
10
10
|
} from 'lucide-react'
|
|
11
11
|
import type { ChatSession, Message } from '../types'
|
|
12
|
-
import { toolLabel, toolSummary } from '../
|
|
12
|
+
import { toolLabel, toolSummary } from '../lib/tool-meta'
|
|
13
13
|
import { cn } from '../lib/utils'
|
|
14
14
|
import { LogoMark } from './Logo'
|
|
15
15
|
|