@bibobonking/opensol 3.1.6 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -7
- package/README.md +24 -20
- package/desktop/README.md +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 +3 -6
- 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 -8
- 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/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { planCompaction, collectFiles, clip, chunkParts, previousSummary, transcriptParts, tokensOf } from './compact-plan.ts'
|
|
4
|
+
|
|
5
|
+
const user = (id, text = 'x'.repeat(300)) => ({ id, role: 'user', content: text, createdAt: 0 })
|
|
6
|
+
const asst = (id, calls = [], content = 'ok') => ({ id, role: 'assistant', content, reasoning: '', toolCalls: calls, round: 0, createdAt: 0 })
|
|
7
|
+
const tool = (id, name = 'read_file', result = 'r'.repeat(300)) => ({ id, role: 'tool', call: { id: `${id}c`, name, args: {} }, status: 'done', result, round: 0, createdAt: 0 })
|
|
8
|
+
|
|
9
|
+
const turn = (n) => [user(`u${n}`), asst(`a${n}`, [{ id: `c${n}`, name: 'read_file', args: {} }]), tool(`t${n}`), asst(`b${n}`)]
|
|
10
|
+
|
|
11
|
+
test('keeps the newest turns that fit and cuts at a user message', () => {
|
|
12
|
+
const messages = [1, 2, 3, 4, 5].flatMap(turn)
|
|
13
|
+
const plan = planCompaction(messages, tokensOf(turn(1)) * 2 + 10)
|
|
14
|
+
assert.ok(plan)
|
|
15
|
+
assert.equal(plan.tail[0].role, 'user')
|
|
16
|
+
assert.equal(plan.tail.filter((m) => m.role === 'user').length, 2)
|
|
17
|
+
assert.equal(plan.head.length + plan.tail.length, messages.length)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('never separates a tool call from its result', () => {
|
|
21
|
+
const messages = [1, 2, 3].flatMap(turn)
|
|
22
|
+
const plan = planCompaction(messages, 1)
|
|
23
|
+
assert.ok(plan)
|
|
24
|
+
assert.equal(plan.tail.length, 4) // only the newest turn, whole
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('returns null when there is nothing to cut', () => {
|
|
28
|
+
assert.equal(planCompaction(turn(1), 10_000), null)
|
|
29
|
+
assert.equal(planCompaction([], 100), null)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('tracks written files once, most recent last', () => {
|
|
33
|
+
const calls = (name, path) => asst('a', [{ id: path + name, name, args: { path } }])
|
|
34
|
+
const files = collectFiles([calls('write_file', 'a.ts'), calls('read_file', 'z.ts'), calls('edit_file', 'b.ts'), calls('edit_file', 'a.ts')], ['old.ts'])
|
|
35
|
+
assert.deepEqual(files, ['old.ts', 'b.ts', 'a.ts'])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('clip keeps head and tail', () => {
|
|
39
|
+
const text = `START${'m'.repeat(1000)}END`
|
|
40
|
+
const out = clip(text, 100)
|
|
41
|
+
assert.ok(out.startsWith('START') && out.endsWith('END') && out.length < 200)
|
|
42
|
+
assert.equal(clip('short', 100), 'short')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('chunks never split a part and respect the size', () => {
|
|
46
|
+
const chunks = chunkParts(['a'.repeat(50), 'b'.repeat(50), 'c'.repeat(50)], 120)
|
|
47
|
+
assert.equal(chunks.length, 2)
|
|
48
|
+
assert.ok(chunks[0].includes('a') && chunks[0].includes('b') && !chunks[0].includes('c'))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('previousSummary reads a [task, summary] pair', () => {
|
|
52
|
+
const summary = { ...asst('s'), content: 'SUM', compaction: { files: ['f.ts'], turns: 2, toolCalls: 3, at: 1, trigger: 'auto' } }
|
|
53
|
+
assert.deepEqual(previousSummary([user('u'), summary, user('u2')]), { text: 'SUM', files: ['f.ts'], skip: 2 })
|
|
54
|
+
assert.equal(previousSummary([user('u'), asst('a')]), null)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('read_file output is trimmed harder than shell output in transcripts', () => {
|
|
58
|
+
const big = 'q'.repeat(5000)
|
|
59
|
+
const [read] = transcriptParts([tool('t1', 'read_file', big)])
|
|
60
|
+
const [shell] = transcriptParts([tool('t2', 'shell', big)])
|
|
61
|
+
assert.ok(read.length < shell.length)
|
|
62
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for smart compaction (no stores, no network, so node:test can run them).
|
|
3
|
+
*
|
|
4
|
+
* Strategy: keep the most recent turns verbatim, summarise everything older, and carry the
|
|
5
|
+
* previous summary forward instead of summarising a summary. File paths touched are tracked
|
|
6
|
+
* deterministically, so the model cannot forget them.
|
|
7
|
+
*/
|
|
8
|
+
import type { AssistantMessage, Message, ToolMessage, UserMessage } from '../types'
|
|
9
|
+
|
|
10
|
+
const CHARS_PER_TOKEN = 3
|
|
11
|
+
|
|
12
|
+
export const WRITE_TOOLS = new Set(['write_file', 'edit_file', 'rewrite_file'])
|
|
13
|
+
|
|
14
|
+
export function messageTokens(message: Message): number {
|
|
15
|
+
if (message.role === 'user') {
|
|
16
|
+
const extra = (message.attachments ?? []).reduce((sum, item) => sum + (item.text?.length ?? 0), 0)
|
|
17
|
+
return Math.ceil((message.content.length + extra) / CHARS_PER_TOKEN) + 4
|
|
18
|
+
}
|
|
19
|
+
if (message.role === 'assistant') {
|
|
20
|
+
const calls = message.toolCalls.reduce((sum, call) => sum + JSON.stringify(call.args).length + call.name.length + 10, 0)
|
|
21
|
+
return Math.ceil((message.content.length + message.reasoning.length + calls) / CHARS_PER_TOKEN) + 4
|
|
22
|
+
}
|
|
23
|
+
return Math.ceil((message.result ?? '').length / CHARS_PER_TOKEN) + 4
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tokensOf(messages: Message[]): number {
|
|
27
|
+
return messages.reduce((sum, message) => sum + messageTokens(message), 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Keeps the start and the end of long text, because the end of a log usually matters most. */
|
|
31
|
+
export function clip(text: string, limit: number, tailRatio = 0.35): string {
|
|
32
|
+
if (text.length <= limit) return text
|
|
33
|
+
const tail = Math.floor(limit * tailRatio)
|
|
34
|
+
const head = limit - tail
|
|
35
|
+
return `${text.slice(0, head)}\n… [${text.length - limit} characters omitted] …\n${text.slice(text.length - tail)}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSummaryMessage(message: Message | undefined): message is AssistantMessage {
|
|
39
|
+
return Boolean(message && message.role === 'assistant' && message.compaction)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CompactionPlan {
|
|
43
|
+
/** Messages [0, cut) are summarised; [cut, end) stay verbatim. */
|
|
44
|
+
cut: number
|
|
45
|
+
head: Message[]
|
|
46
|
+
tail: Message[]
|
|
47
|
+
/** The cut had to land inside the running turn because older turns alone were too small. */
|
|
48
|
+
midTurn: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Chooses where to cut. The tail always starts at a user message (so provider alternation stays
|
|
53
|
+
* valid and tool calls never lose their results) and is the newest run of turns that fits in
|
|
54
|
+
* `keepTokens`. The newest turn is always kept. Returns null when there is nothing worth cutting.
|
|
55
|
+
*/
|
|
56
|
+
export function planCompaction(messages: Message[], keepTokens: number, minHeadMessages = 4): CompactionPlan | null {
|
|
57
|
+
const userIndexes: number[] = []
|
|
58
|
+
messages.forEach((message, index) => {
|
|
59
|
+
if (message.role === 'user') userIndexes.push(index)
|
|
60
|
+
})
|
|
61
|
+
if (userIndexes.length === 0) return null
|
|
62
|
+
|
|
63
|
+
// Walk turns from the newest backwards while they fit in the keep budget.
|
|
64
|
+
let cut = userIndexes[userIndexes.length - 1]
|
|
65
|
+
let kept = tokensOf(messages.slice(cut))
|
|
66
|
+
for (let i = userIndexes.length - 2; i >= 0; i -= 1) {
|
|
67
|
+
const turnTokens = tokensOf(messages.slice(userIndexes[i], userIndexes[i + 1]))
|
|
68
|
+
if (kept + turnTokens > keepTokens) break
|
|
69
|
+
kept += turnTokens
|
|
70
|
+
cut = userIndexes[i]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip an existing summary pair (task message + summary) at the very start: it is always head.
|
|
74
|
+
if (cut < minHeadMessages) return null
|
|
75
|
+
return { cut, head: messages.slice(0, cut), tail: messages.slice(cut), midTurn: false }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Previous summary text, when the head starts with `[task user message, summary]`. */
|
|
79
|
+
export function previousSummary(head: Message[]): { text: string; files: string[]; skip: number } | null {
|
|
80
|
+
const summary = head[1]
|
|
81
|
+
if (head[0]?.role === 'user' && isSummaryMessage(summary)) {
|
|
82
|
+
return { text: summary.content, files: summary.compaction?.files ?? [], skip: 2 }
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Paths written or edited by tool calls, newest last, de-duplicated. */
|
|
88
|
+
export function collectFiles(messages: Message[], seed: string[] = []): string[] {
|
|
89
|
+
const seen = new Set(seed)
|
|
90
|
+
const ordered = [...seed]
|
|
91
|
+
for (const message of messages) {
|
|
92
|
+
if (message.role !== 'assistant') continue
|
|
93
|
+
for (const call of message.toolCalls) {
|
|
94
|
+
const path = typeof call.args.path === 'string' ? call.args.path : ''
|
|
95
|
+
if (!path || !WRITE_TOOLS.has(call.name)) continue
|
|
96
|
+
if (seen.has(path)) {
|
|
97
|
+
ordered.splice(ordered.indexOf(path), 1)
|
|
98
|
+
}
|
|
99
|
+
seen.add(path)
|
|
100
|
+
ordered.push(path)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return ordered
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toolResultLimit(message: ToolMessage): number {
|
|
107
|
+
const name = message.call.name
|
|
108
|
+
if (message.status === 'error') return 1500
|
|
109
|
+
if (name === 'read_file') return 700 // contents can be re-read; keep only the shape
|
|
110
|
+
if (name === 'write_file' || name === 'edit_file' || name === 'rewrite_file') return 400
|
|
111
|
+
if (name === 'shell') return 2500
|
|
112
|
+
return 1500
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function callSummary(args: Record<string, unknown>): string {
|
|
116
|
+
const text = JSON.stringify(args)
|
|
117
|
+
return text.length > 600 ? `${text.slice(0, 600)}…` : text
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Transcript lines, one entry per message, so they can be chunked without splitting a message. */
|
|
121
|
+
export function transcriptParts(messages: Message[]): string[] {
|
|
122
|
+
const parts: string[] = []
|
|
123
|
+
for (const message of messages) {
|
|
124
|
+
if (message.role === 'user') {
|
|
125
|
+
const names = (message.attachments ?? []).map((item) => item.name)
|
|
126
|
+
parts.push(`[USER]\n${clip(message.content, 6000)}${names.length ? `\n[Attachments: ${names.join(', ')}]` : ''}`)
|
|
127
|
+
} else if (message.role === 'assistant') {
|
|
128
|
+
const lines: string[] = []
|
|
129
|
+
if (message.content.trim()) lines.push(clip(message.content.trim(), 4000))
|
|
130
|
+
for (const call of message.toolCalls) lines.push(`→ ${call.name} ${callSummary(call.args)}`)
|
|
131
|
+
if (lines.length) parts.push(`[ASSISTANT]\n${lines.join('\n')}`)
|
|
132
|
+
} else {
|
|
133
|
+
const status = message.status === 'done' ? 'ok' : message.status
|
|
134
|
+
parts.push(`[RESULT ${message.call.name} · ${status}]\n${clip(message.result ?? '', toolResultLimit(message))}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return parts
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Groups transcript parts into chunks under `maxChars`; one oversized part gets its own chunk. */
|
|
141
|
+
export function chunkParts(parts: string[], maxChars: number): string[] {
|
|
142
|
+
const chunks: string[] = []
|
|
143
|
+
let current: string[] = []
|
|
144
|
+
let size = 0
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
if (current.length && size + part.length > maxChars) {
|
|
147
|
+
chunks.push(current.join('\n\n---\n\n'))
|
|
148
|
+
current = []
|
|
149
|
+
size = 0
|
|
150
|
+
}
|
|
151
|
+
current.push(part)
|
|
152
|
+
size += part.length + 9
|
|
153
|
+
}
|
|
154
|
+
if (current.length) chunks.push(current.join('\n\n---\n\n'))
|
|
155
|
+
return chunks
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Used when the model cannot summarise: a lossy but deterministic record of the session. */
|
|
159
|
+
export function fallbackSummary(head: Message[], previous: string, files: string[]): string {
|
|
160
|
+
const asks = head
|
|
161
|
+
.filter((message): message is UserMessage => message.role === 'user')
|
|
162
|
+
.map((message) => `- ${clip(message.content.trim().replace(/\s+/g, ' '), 300)}`)
|
|
163
|
+
const lastAnswer = [...head]
|
|
164
|
+
.reverse()
|
|
165
|
+
.find((message): message is AssistantMessage => message.role === 'assistant' && Boolean(message.content.trim()))
|
|
166
|
+
const toolCount = head.filter((message) => message.role === 'tool').length
|
|
167
|
+
return [
|
|
168
|
+
previous ? `## Earlier summary\n${previous}` : '',
|
|
169
|
+
'## User requests so far',
|
|
170
|
+
asks.slice(-12).join('\n') || '- (none)',
|
|
171
|
+
lastAnswer ? `## Last assistant conclusion\n${clip(lastAnswer.content.trim(), 1500)}` : '',
|
|
172
|
+
`## Activity\n${toolCount} tool calls were made. This summary was built without the model, so details may be missing: re-read files before relying on them.`,
|
|
173
|
+
files.length ? `## Files changed\n${files.map((file) => `- ${file}`).join('\n')}` : '',
|
|
174
|
+
]
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join('\n\n')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A short, verbatim look at the newest steps, for compaction in the middle of a running turn. */
|
|
180
|
+
export function recentSteps(messages: Message[], count = 4): string {
|
|
181
|
+
const steps: string[] = []
|
|
182
|
+
for (let i = messages.length - 1; i >= 0 && steps.length < count; i -= 1) {
|
|
183
|
+
const message = messages[i]
|
|
184
|
+
if (message.role !== 'tool') continue
|
|
185
|
+
steps.push(`• ${message.call.name} ${callSummary(message.call.args)} → ${message.status}\n${clip(message.result ?? '', 1200)}`)
|
|
186
|
+
}
|
|
187
|
+
return steps.reverse().join('\n\n')
|
|
188
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolName } from '../types'
|
|
2
|
+
|
|
3
|
+
export type SubagentType = 'explore' | 'research' | 'review'
|
|
4
|
+
|
|
5
|
+
export interface SubagentSpec {
|
|
6
|
+
label: string
|
|
7
|
+
/** One line for the main agent's tool description. */
|
|
8
|
+
summary: string
|
|
9
|
+
/** Built-in tools this kind of subagent may call. All are read-only in Plan mode. */
|
|
10
|
+
tools: ToolName[]
|
|
11
|
+
/** Hard cap on model rounds; the subagent is then forced to report. */
|
|
12
|
+
maxRounds: number
|
|
13
|
+
guidance: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SUBAGENT_TYPES: Record<SubagentType, SubagentSpec> = {
|
|
17
|
+
explore: {
|
|
18
|
+
label: 'Explore',
|
|
19
|
+
summary: 'fast, local: find where something lives, trace a flow or a bug through the codebase',
|
|
20
|
+
tools: ['read_file', 'list_files', 'grep', 'shell', 'git', 'skill'],
|
|
21
|
+
maxRounds: 24,
|
|
22
|
+
guidance: [
|
|
23
|
+
'Search wide first (grep, list_files), then read only the files that matter.',
|
|
24
|
+
'Run independent searches in the same round instead of one after another.',
|
|
25
|
+
'Report exact paths with line numbers and a short quote or code reference for each claim.',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
research: {
|
|
29
|
+
label: 'Research',
|
|
30
|
+
summary: 'look things up on the web (docs, release notes, errors) and cross-check with the local code',
|
|
31
|
+
tools: ['read_file', 'list_files', 'grep', 'web_search', 'read_url', 'skill'],
|
|
32
|
+
maxRounds: 18,
|
|
33
|
+
guidance: [
|
|
34
|
+
'Prefer primary sources (official docs, changelogs, source repositories). Open the pages you cite.',
|
|
35
|
+
'Cross-check important claims with a second source or with the local code.',
|
|
36
|
+
'Report each finding with its URL, and say plainly what you could not confirm.',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
review: {
|
|
40
|
+
label: 'Review',
|
|
41
|
+
summary: 'critically review code or a diff for bugs, regressions, missing tests and risky changes',
|
|
42
|
+
tools: ['read_file', 'list_files', 'grep', 'shell', 'git', 'skill'],
|
|
43
|
+
maxRounds: 30,
|
|
44
|
+
guidance: [
|
|
45
|
+
'Start from `git status` and `git diff` when the task is about recent changes; otherwise read the named files.',
|
|
46
|
+
'Check call sites and edge cases, not just the changed lines. Verify a suspected bug by reading the code it touches.',
|
|
47
|
+
'Report findings ordered by severity, each with file:line, why it is a problem and a concrete fix. Say "no issues found" if that is the truth.',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function subagentType(value: unknown): SubagentType {
|
|
53
|
+
return value === 'research' || value === 'review' ? value : 'explore'
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BuiltinToolName, ToolName } from '../types'
|
|
2
|
+
|
|
3
|
+
export function toolLabel(name: ToolName): string {
|
|
4
|
+
if (name.startsWith('mcp__')) return name.split('__').slice(2).join(' ').replace(/_/g, ' ') || 'MCP tool'
|
|
5
|
+
const labels: Record<BuiltinToolName, string> = {
|
|
6
|
+
shell: 'Shell',
|
|
7
|
+
read_file: 'Read file',
|
|
8
|
+
list_files: 'List files',
|
|
9
|
+
write_file: 'Write file',
|
|
10
|
+
edit_file: 'Edit file',
|
|
11
|
+
rewrite_file: 'Rewrite file',
|
|
12
|
+
grep: 'Grep',
|
|
13
|
+
web_search: 'Web search',
|
|
14
|
+
read_url: 'Read URL',
|
|
15
|
+
git: 'Git',
|
|
16
|
+
subagent: 'Subagent',
|
|
17
|
+
skill: 'Skill',
|
|
18
|
+
}
|
|
19
|
+
return labels[name as BuiltinToolName] ?? 'Tool'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toolSummary(name: ToolName, args: Record<string, unknown>): string {
|
|
23
|
+
const value =
|
|
24
|
+
name === 'shell'
|
|
25
|
+
? args.command
|
|
26
|
+
: name === 'grep'
|
|
27
|
+
? args.pattern
|
|
28
|
+
: name === 'web_search'
|
|
29
|
+
? args.query
|
|
30
|
+
: name === 'read_url'
|
|
31
|
+
? args.url
|
|
32
|
+
: name === 'git'
|
|
33
|
+
? args.action
|
|
34
|
+
: name === 'subagent'
|
|
35
|
+
? args.task
|
|
36
|
+
: name === 'skill'
|
|
37
|
+
? [args.name, args.file].filter(Boolean).join(' / ')
|
|
38
|
+
: (args.path ?? '')
|
|
39
|
+
return typeof value === 'string' ? value : ''
|
|
40
|
+
}
|
package/src/store/settings.ts
CHANGED
|
@@ -79,6 +79,7 @@ const DEFAULTS: AppSettings = {
|
|
|
79
79
|
workDetail: 'detailed',
|
|
80
80
|
autoCollapseWork: true,
|
|
81
81
|
aiSessionTitles: true,
|
|
82
|
+
showMascot: false,
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
const EFFORTS = new Set<Effort>(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
@@ -187,6 +188,7 @@ function buildSettings(persisted?: Partial<AppSettings>): AppSettings {
|
|
|
187
188
|
workDetail: saved.workDetail === 'concise' ? 'concise' : 'detailed',
|
|
188
189
|
autoCollapseWork: saved.autoCollapseWork !== false,
|
|
189
190
|
aiSessionTitles: saved.aiSessionTitles !== false,
|
|
191
|
+
showMascot: saved.showMascot === true,
|
|
190
192
|
workspacePrompts: sanitizeWorkspacePrompts(saved.workspacePrompts),
|
|
191
193
|
}
|
|
192
194
|
}
|
|
@@ -216,6 +218,7 @@ function persistedPatch(patch: Partial<AppSettings>): Partial<AppSettings> {
|
|
|
216
218
|
'workDetail',
|
|
217
219
|
'autoCollapseWork',
|
|
218
220
|
'aiSessionTitles',
|
|
221
|
+
'showMascot',
|
|
219
222
|
'workspacePrompts',
|
|
220
223
|
]
|
|
221
224
|
for (const key of keys) {
|
package/src/store/ui.ts
CHANGED
|
@@ -6,9 +6,21 @@ interface RuntimeState {
|
|
|
6
6
|
/** Sessions whose title the AI is writing right now. */
|
|
7
7
|
titling: Record<string, boolean>
|
|
8
8
|
setTitling: (sessionId: string, active: boolean) => void
|
|
9
|
+
/** Sessions being compacted right now. */
|
|
10
|
+
compacting: Record<string, boolean>
|
|
11
|
+
setCompacting: (sessionId: string, active: boolean) => void
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export const useRuntime = create<RuntimeState>()((set) => ({
|
|
15
|
+
compacting: {},
|
|
16
|
+
setCompacting: (sessionId, active) =>
|
|
17
|
+
set((state) => {
|
|
18
|
+
if (Boolean(state.compacting[sessionId]) === active) return state
|
|
19
|
+
const compacting = { ...state.compacting }
|
|
20
|
+
if (active) compacting[sessionId] = true
|
|
21
|
+
else delete compacting[sessionId]
|
|
22
|
+
return { compacting }
|
|
23
|
+
}),
|
|
12
24
|
titling: {},
|
|
13
25
|
setTitling: (sessionId, active) =>
|
|
14
26
|
set((state) => {
|
package/src/types.ts
CHANGED
|
@@ -150,6 +150,8 @@ export interface AppSettings {
|
|
|
150
150
|
autoCollapseWork: boolean
|
|
151
151
|
/** Let the model name a session after its first answer. */
|
|
152
152
|
aiSessionTitles: boolean
|
|
153
|
+
/** Show the pixel mascot on the home screen and above the composer. */
|
|
154
|
+
showMascot: boolean
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
export interface Attachment {
|
|
@@ -203,6 +205,20 @@ export interface AssistantMessage {
|
|
|
203
205
|
createdAt: number
|
|
204
206
|
/** Tool calls in this round already have follow-up tool messages. */
|
|
205
207
|
round: number
|
|
208
|
+
/** Set on the summary message written by compaction; `content` is the summary. */
|
|
209
|
+
compaction?: CompactionMeta
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface CompactionMeta {
|
|
213
|
+
/** Files written or edited before this point, tracked from tool calls (not model-written). */
|
|
214
|
+
files: string[]
|
|
215
|
+
/** User turns and tool calls folded into the summary. */
|
|
216
|
+
turns: number
|
|
217
|
+
toolCalls: number
|
|
218
|
+
at: number
|
|
219
|
+
trigger: 'auto' | 'manual' | 'mid-turn'
|
|
220
|
+
/** The model was unavailable, so the summary was built without it. */
|
|
221
|
+
fallback?: boolean
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
/** Line-level change report for a file edit: +added, −removed and where it started (1-based). */
|
|
@@ -358,6 +374,11 @@ export interface DaemonInfo {
|
|
|
358
374
|
dataDir: string
|
|
359
375
|
home: string
|
|
360
376
|
platform: string
|
|
377
|
+
/** process.platform of the daemon: win32, darwin or linux. */
|
|
378
|
+
os?: string
|
|
379
|
+
arch?: string
|
|
380
|
+
/** Shell the agent's commands run in, e.g. "zsh on macOS (arm64)". */
|
|
381
|
+
shell?: string
|
|
361
382
|
node: string
|
|
362
383
|
}
|
|
363
384
|
|
|
@@ -412,6 +433,11 @@ export interface DaemonSnapshot {
|
|
|
412
433
|
dataDir: string
|
|
413
434
|
home: string
|
|
414
435
|
platform: string
|
|
436
|
+
/** process.platform of the daemon: win32, darwin or linux. */
|
|
437
|
+
os?: string
|
|
438
|
+
arch?: string
|
|
439
|
+
/** Shell the agent's commands run in, e.g. "zsh on macOS (arm64)". */
|
|
440
|
+
shell?: string
|
|
415
441
|
node: string
|
|
416
442
|
settings: Partial<AppSettings>
|
|
417
443
|
chats: { sessions: ChatSession[]; activeId: string | null }
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/App.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/agent/approval.ts","./src/agent/compact.ts","./src/agent/index.ts","./src/agent/model.ts","./src/agent/prompt.ts","./src/agent/queue.ts","./src/agent/subagent.ts","./src/agent/tools.ts","./src/agent/adapters/anthropic.ts","./src/agent/adapters/gemini.ts","./src/agent/adapters/openai-responses.ts","./src/agent/adapters/openai.ts","./src/agent/adapters/shared.ts","./src/components/ChatView.tsx","./src/components/CommandPalette.tsx","./src/components/Composer.tsx","./src/components/ContextBar.tsx","./src/components/DeployModal.tsx","./src/components/DiffView.tsx","./src/components/ErrorBoundary.tsx","./src/components/FileIcon.tsx","./src/components/HomeDashboard.tsx","./src/components/Logo.tsx","./src/components/Markdown.tsx","./src/components/McpPanel.tsx","./src/components/MessageItem.tsx","./src/components/ModelDeck.tsx","./src/components/PixleSol.tsx","./src/components/PluginBuilder.tsx","./src/components/PluginManager.tsx","./src/components/PreviewPane.tsx","./src/components/SessionsTab.tsx","./src/components/SettingsModal.tsx","./src/components/Sidebar.tsx","./src/components/Splash.tsx","./src/components/SystemPromptButton.tsx","./src/components/ToolCallCard.tsx","./src/components/TopBar.tsx","./src/components/TrajectoryView.tsx","./src/components/WorkspacePanel.tsx","./src/components/ui.tsx","./src/core/kernel.ts","./src/core/mcp-tools.ts","./src/core/types.ts","./src/core/plugins/tools/fs.ts","./src/core/plugins/tools/git.ts","./src/core/plugins/tools/grep.ts","./src/core/plugins/tools/shell.ts","./src/core/plugins/tools/skill.ts","./src/core/plugins/tools/subagent.ts","./src/core/plugins/tools/web.ts","./src/core/services/agent-loop.ts","./src/core/services/approval.ts","./src/core/services/compaction.ts","./src/core/services/llm.ts","./src/core/services/sessions.ts","./src/core/services/subagents.ts","./src/core/services/system-prompt.ts","./src/core/services/titles.ts","./src/core/services/tools.ts","./src/core/services/ui.ts","./src/data/providers.ts","./src/lib/boot.ts","./src/lib/context.ts","./src/lib/conversation.ts","./src/lib/credits.ts","./src/lib/daemon.ts","./src/lib/diff.ts","./src/lib/hooks.ts","./src/lib/mcp-config.ts","./src/lib/permissions.ts","./src/lib/pixlesol.ts","./src/lib/plugin-folder.ts","./src/lib/reasoning.ts","./src/lib/retry.ts","./src/lib/sse.ts","./src/lib/system-prompt.ts","./src/lib/themes.ts","./src/lib/usage.ts","./src/lib/utils.ts","./src/plugins/api.ts","./src/plugins/bridge.d.mts","./src/plugins/index.ts","./src/plugins/preview.ts","./src/plugins/runtime.ts","./src/plugins/types.ts","./src/store/chats.ts","./src/store/mcp.ts","./src/store/models.ts","./src/store/settings.ts","./src/store/skills.ts","./src/store/ui.ts","./src/store/workspace.ts"],"version":"5.9.3"}
|