Skip to content

Commit 62873bf

Browse files
committed
strip away the physical smart hardware part and focus on the code assistant
1 parent ec22312 commit 62873bf

111 files changed

Lines changed: 3263 additions & 7854 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,6 @@ ttf-parser = { version = "0.25", optional = true }
207207
subsetter = { version = "0.2", optional = true }
208208

209209
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dependencies]
210-
nusb = { version = "0.2", default-features = false, optional = true }
211-
212210
probe-rs = { version = "0.31", optional = true }
213211

214212
pdf-extract = { version = "0.10", optional = true }
@@ -304,7 +302,7 @@ code-intel-java = ["dep:tree-sitter-java"]
304302
code-intel-c = ["dep:tree-sitter-c"]
305303
code-intel-cpp = ["dep:tree-sitter-cpp"]
306304
channel-nostr = ["dep:nostr-sdk"]
307-
hardware = ["nusb", "tokio-serial"]
305+
hardware = ["tokio-serial"]
308306
channel-matrix = ["dep:matrix-sdk"]
309307
channel-lark = ["dep:prost"]
310308
channel-feishu = ["channel-lark"]

‎desktop/src/api/evolution.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ export const evolutionApi = {
3434
api.put<EvolutionLesson>(`/api/evolution/lessons/${encodeURIComponent(id)}`, patch),
3535
deleteLesson: (id: string) => api.delete<{ ok: boolean }>(`/api/evolution/lessons/${encodeURIComponent(id)}`),
3636

37-
recordThumb: (sessionId: string, turnId: string, score: 1 | 0 | -1, comment?: string) =>
38-
api.post<{ ok: boolean; voteId: string; rewardDelta: number }>('/api/evolution/thumbs', {
39-
session_id: sessionId,
40-
turn_id: turnId,
41-
score,
42-
comment,
43-
}),
37+
recordThumb: (sessionId: string, score: 1 | 0 | -1, turnId?: string, comment?: string) =>
38+
api.post<{ ok: boolean; voteId: string; finalReward: number; auditOnly: boolean }>(
39+
'/api/evolution/thumbs',
40+
{
41+
session_id: sessionId,
42+
turn_id: turnId,
43+
score,
44+
comment,
45+
},
46+
),
4447

4548
distillTurn: (turnId: string) =>
4649
api.post<{ ok: boolean; queued: boolean; turnId: string }>('/api/evolution/distill', {

‎desktop/src/components/chat/AssistantMessage.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Props = {
1414
isStreaming?: boolean
1515

1616
assistantTurnCopyText?: string
17+
showThumbs?: boolean
1718
sessionId?: string | null
1819
workDir?: string | null
1920
disableFork?: boolean
@@ -94,6 +95,7 @@ export const AssistantMessage = memo(function AssistantMessage({
9495
content,
9596
isStreaming,
9697
assistantTurnCopyText,
98+
showThumbs,
9799
sessionId,
98100
workDir,
99101
disableFork,
@@ -155,6 +157,7 @@ export const AssistantMessage = memo(function AssistantMessage({
155157
<div className="mt-0.5 flex w-full justify-end pr-5">
156158
<AssistantMessageActions
157159
copyText={assistantTurnCopyText ?? ''}
160+
showThumbs={showThumbs}
158161
sessionId={sessionId}
159162
workDir={workDir}
160163
disableFork={disableFork}

‎desktop/src/components/chat/AssistantMessageActions.tsx‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,58 @@ import { createPortal } from 'react-dom'
77
import { useTranslation } from '../../i18n'
88
import { copyTextToClipboard } from './clipboard'
99
import { useAnchoredDropdown } from '../../hooks/useAnchoredDropdown'
10+
import { evolutionApi } from '../../api/evolution'
1011
import { useSessionStore } from '../../stores/sessionStore'
1112
import { useTabStore } from '../../stores/tabStore'
1213
import { useUIStore } from '../../stores/uiStore'
1314

1415
type Props = {
1516
copyText: string
17+
showThumbs?: boolean
1618
sessionId?: string | null
1719
workDir?: string | null
1820
disableFork?: boolean
1921
}
2022

21-
export function AssistantMessageActions({ copyText, sessionId, workDir, disableFork }: Props) {
23+
export function AssistantMessageActions({
24+
copyText,
25+
showThumbs,
26+
sessionId,
27+
workDir,
28+
disableFork,
29+
}: Props) {
2230
const t = useTranslation()
2331
const addToast = useUIStore((s) => s.addToast)
2432
const [open, setOpen] = useState(false)
33+
const [votedScore, setVotedScore] = useState<1 | -1 | null>(null)
34+
const [voting, setVoting] = useState(false)
2535
const { triggerRef, menuRef, style, portalTarget } = useAnchoredDropdown<HTMLButtonElement>(
2636
open,
2737
() => setOpen(false),
2838
{ align: 'right', estimatedHeight: 110 },
2939
)
3040

41+
const submitThumb = useCallback(
42+
async (score: 1 | -1) => {
43+
const sid = sessionId?.trim()
44+
if (!sid || voting || votedScore === score) return
45+
setVoting(true)
46+
try {
47+
await evolutionApi.recordThumb(sid, score)
48+
setVotedScore(score)
49+
addToast({ type: 'success', message: t('chat.thumbRecordedToast') })
50+
} catch (e) {
51+
addToast({
52+
type: 'error',
53+
message: e instanceof Error ? e.message : String(e),
54+
})
55+
} finally {
56+
setVoting(false)
57+
}
58+
},
59+
[addToast, sessionId, t, votedScore, voting],
60+
)
61+
3162
const copyMessage = useCallback(async () => {
3263
const ok = await copyTextToClipboard(copyText)
3364
if (ok) addToast({ type: 'success', message: t('chat.copyMessageToast') })
@@ -52,8 +83,55 @@ export function AssistantMessageActions({ copyText, sessionId, workDir, disableF
5283
const itemClass =
5384
'flex w-full px-3 py-2.5 text-left text-[13px] text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)] disabled:cursor-not-allowed disabled:opacity-45'
5485

86+
const thumbClass = (active: boolean) =>
87+
`inline-flex h-5 w-5 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 ${
88+
active
89+
? 'text-[var(--color-brand)]'
90+
: 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
91+
}`
92+
5593
return (
56-
<div className="relative flex w-full justify-end">
94+
<div className="relative flex w-full items-center justify-end gap-0.5">
95+
{Boolean(showThumbs) && Boolean(sessionId?.trim()) && (
96+
<>
97+
<button
98+
type="button"
99+
className={thumbClass(votedScore === 1)}
100+
aria-label={t('chat.thumbUp')}
101+
title={t('chat.thumbUp')}
102+
disabled={voting}
103+
onClick={() => void submitThumb(1)}
104+
>
105+
<span
106+
className="material-symbols-outlined text-[12px] leading-none"
107+
style={{
108+
fontVariationSettings: `'wght' 300, 'opsz' 20, 'GRAD' 0, 'FILL' ${votedScore === 1 ? 1 : 0}`,
109+
}}
110+
aria-hidden="true"
111+
>
112+
thumb_up
113+
</span>
114+
</button>
115+
<button
116+
type="button"
117+
className={thumbClass(votedScore === -1)}
118+
aria-label={t('chat.thumbDown')}
119+
title={t('chat.thumbDown')}
120+
disabled={voting}
121+
onClick={() => void submitThumb(-1)}
122+
>
123+
<span
124+
className="material-symbols-outlined text-[12px] leading-none"
125+
style={{
126+
fontVariationSettings: `'wght' 300, 'opsz' 20, 'GRAD' 0, 'FILL' ${votedScore === -1 ? 1 : 0}`,
127+
}}
128+
aria-hidden="true"
129+
>
130+
thumb_down
131+
</span>
132+
</button>
133+
</>
134+
)}
57135
<button
58136
ref={triggerRef}
59137
type="button"

‎desktop/src/components/chat/MessageList.tsx‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ function appendChildToolCall(
189189

190190
const supersededTodoCloneCache = new WeakMap<UIMessage, UIMessage>()
191191

192+
const orphanToolUsePlaceholderCache = new WeakMap<UIMessage, UIMessage>()
193+
192194
export function buildRenderModel(
193195
messages: UIMessage[],
194196
pendingAskToolUseId?: string | null,
@@ -246,9 +248,23 @@ export function buildRenderModel(
246248
continue
247249
}
248250
if (seenToolUseIds.has(msg.toolUseId)) continue
251+
if (emittedToolUseIds.has(msg.toolUseId)) continue
249252

250253
flush()
251-
items.push({ kind: 'message', message: msg })
254+
let placeholder = orphanToolUsePlaceholderCache.get(msg)
255+
if (!placeholder) {
256+
placeholder = {
257+
id: `orphan-tool-use-${msg.toolUseId}`,
258+
type: 'tool_use',
259+
toolName: 'tool',
260+
toolUseId: msg.toolUseId,
261+
input: {},
262+
timestamp: msg.timestamp,
263+
}
264+
orphanToolUsePlaceholderCache.set(msg, placeholder)
265+
}
266+
items.push({ kind: 'message', message: placeholder })
267+
emittedToolUseIds.add(msg.toolUseId)
252268
continue
253269
}
254270

@@ -1158,6 +1174,14 @@ export function MessageList({ sessionId }: MessageListProps = {}) {
11581174
[messages],
11591175
)
11601176

1177+
const lastAssistantTextMsgId = useMemo(() => {
1178+
for (let i = messages.length - 1; i >= 0; i--) {
1179+
const m = messages[i]
1180+
if (m && m.type === 'assistant_text') return m.id
1181+
}
1182+
return null
1183+
}, [messages])
1184+
11611185
const isTailRendering = useMemo(() => {
11621186
if (streamingText.trim().length > 0) return true
11631187
if (hasLiveThinkingContent) return true
@@ -1470,6 +1494,7 @@ export function MessageList({ sessionId }: MessageListProps = {}) {
14701494
chatState === 'idle' &&
14711495
Boolean(turnCopy?.isLastAssistantSegmentInTurn) &&
14721496
Boolean((turnCopy?.fullText ?? '').trim())
1497+
const showThumbs = tailMenuEnabled && msg.id === lastAssistantTextMsgId
14731498

14741499
const block = (
14751500
<SectionErrorBoundary key={msg.id} label="message" resetKeys={[msg.id]}>
@@ -1478,6 +1503,7 @@ export function MessageList({ sessionId }: MessageListProps = {}) {
14781503
activeThinkingId={activeThinkingId}
14791504
toolStreaming={toolStreaming}
14801505
tailMenuEnabled={tailMenuEnabled}
1506+
showThumbs={showThumbs}
14811507
assistantTurnCopy={turnCopy}
14821508
sessionId={resolvedSessionId}
14831509
sessionWorkDir={activeSessionMeta?.workDir ?? null}
@@ -1519,6 +1545,7 @@ export function MessageList({ sessionId }: MessageListProps = {}) {
15191545
restoreAnchorMsgId,
15201546
editingMessage,
15211547
assistantTurnCopyByMsgId,
1548+
lastAssistantTextMsgId,
15221549
childToolCallsByParent,
15231550
childResultsByParent,
15241551
activeSessionMeta?.workDir,
@@ -1830,6 +1857,7 @@ type MessageBlockProps = {
18301857
toolResult?: { content: unknown; isError: boolean } | null
18311858
toolStreaming: boolean
18321859
tailMenuEnabled: boolean
1860+
showThumbs?: boolean
18331861
assistantTurnCopy: AssistantTurnCopyInfo | null
18341862
sessionId?: string | null
18351863
sessionWorkDir?: string | null
@@ -1863,6 +1891,7 @@ function areMessageBlockPropsEqual(
18631891
if (prev.onLiveThinkingGrow !== next.onLiveThinkingGrow) return false
18641892
if (prev.toolStreaming !== next.toolStreaming) return false
18651893
if (prev.tailMenuEnabled !== next.tailMenuEnabled) return false
1894+
if (prev.showThumbs !== next.showThumbs) return false
18661895
if (prev.sessionId !== next.sessionId) return false
18671896
if (prev.sessionWorkDir !== next.sessionWorkDir) return false
18681897
if (prev.disableFork !== next.disableFork) return false
@@ -1923,6 +1952,7 @@ export const MessageBlock = memo(function MessageBlock({
19231952
toolResult,
19241953
toolStreaming,
19251954
tailMenuEnabled,
1955+
showThumbs,
19261956
assistantTurnCopy,
19271957
sessionId,
19281958
sessionWorkDir,
@@ -1988,6 +2018,7 @@ export const MessageBlock = memo(function MessageBlock({
19882018
<AssistantMessage
19892019
content={message.content}
19902020
assistantTurnCopyText={allowTailMenu ? fullTurn : undefined}
2021+
showThumbs={showThumbs}
19912022
sessionId={sessionId}
19922023
workDir={sessionWorkDir}
19932024
disableFork={disableFork}

‎desktop/src/components/chat/tools/HardwareToolView.tsx‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,16 @@ function readNumber(input: unknown, keys: string[]): number | undefined {
3737

3838
function hwLabel(toolName: string): string {
3939
switch (toolName) {
40-
case 'device_read_code':
41-
return 'Read code'
42-
case 'device_write_code':
43-
return 'Write code'
44-
case 'device_exec':
45-
return 'Run'
4640
case 'gpio_read':
4741
return 'GPIO read'
4842
case 'gpio_write':
4943
return 'GPIO write'
50-
case 'pico_flash':
51-
return 'Flash'
44+
case 'hardware_board_info':
45+
return 'Board info'
5246
case 'hardware_memory_map':
5347
return 'Memory map'
48+
case 'hardware_memory_read':
49+
return 'Memory read'
5450
default:
5551
return toolName
5652
}

‎desktop/src/i18n/locales/en.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -888,10 +888,12 @@ export const en = {
888888
'settings.evolution.title': 'Self-Evolution',
889889
'settings.evolution.description': 'Self-reinforcing programming learning. The assistant collects turn data, distils lessons and can export datasets for external/cloud RL.',
890890
'settings.evolution.persistence.title': 'Local Training Data Persistence',
891-
'settings.evolution.persistence.description': 'When OFF the assistant only learns in-context: no raw turn data is written to disk. Lessons and playbooks are still persisted because they are the distilled knowledge.',
891+
'settings.evolution.persistence.description': 'When OFF the assistant only learns in-context: no raw turn data is written to disk. Lessons are still persisted because they are the distilled knowledge.',
892892
'settings.evolution.persistence.toggleOn': 'Enabled',
893893
'settings.evolution.persistence.toggleOff': 'Disabled',
894894
'settings.evolution.persistence.usage': 'Disk usage',
895+
'settings.evolution.persistence.jsonlLines': 'Raw turns on disk',
896+
'settings.evolution.persistence.exportsBytes': 'Exports size',
895897
'settings.evolution.persistence.purgeTitle': 'Purge local training data',
896898
'settings.evolution.persistence.purgeWarning': 'This is irreversible. Type I_UNDERSTAND to confirm.',
897899
'settings.evolution.persistence.purgeAll': 'Purge all',
@@ -906,10 +908,6 @@ export const en = {
906908
'settings.evolution.metrics.lessonHits': 'Total injections',
907909
'settings.evolution.metrics.exports': 'Exports',
908910
'settings.evolution.metrics.pushes': 'Push receipts',
909-
'settings.evolution.metrics.toolSearch': 'tool_search calls / activations / blocked',
910-
'settings.evolution.metrics.toolSearchTooltip':
911-
'tool_search invocations · tools activated · activations rejected for safety · average activation latency.',
912-
'settings.evolution.metrics.deferredBuiltin': '{count} stubs deferred',
913911
'settings.evolution.maintenance.title': 'Maintenance',
914912
'settings.evolution.maintenance.description':
915913
'Manually distill lessons or rescore reward signals for stored turns.',
@@ -930,6 +928,7 @@ export const en = {
930928
'settings.evolution.lessons.empty': 'No lessons yet — they will appear after the assistant solves successful turns.',
931929
'settings.evolution.config.title': 'Engine Settings',
932930
'settings.evolution.config.judgeEnabled': 'Async next-state PRM judge',
931+
'settings.evolution.config.distillEnabled': 'Auto-distill high-reward turns',
933932
'settings.evolution.config.autoDistill': 'Auto-distill on session end',
934933
'settings.evolution.config.maxLessons': 'Max lessons in prompt',
935934
'settings.evolution.config.tokenBudget': 'Lesson token budget',
@@ -1085,17 +1084,24 @@ export const en = {
10851084
'settings.evolution.recycling.persistRequired.body':
10861085
'Experience recycling stores harvested turns; enable persistence so the SQLite store is reachable.',
10871086
'settings.evolution.recycling.persistRequired.enable': 'Enable now',
1088-
'settings.evolution.recycling.runtime.todayHarvested': 'Today {count}',
1087+
'settings.evolution.recycling.runtime.todayHarvested': 'Last 24h {count}',
10891088
'settings.evolution.recycling.runtime.totalHarvested': 'Total {count}',
10901089
'settings.evolution.recycling.runtime.lastHarvestAt': 'Last harvest {ts}',
1090+
'settings.evolution.recycling.purgeConfirmTitle': 'Clear all recycled experiences?',
1091+
'settings.evolution.recycling.purgeConfirmBody':
1092+
'This permanently deletes {count} recycled experiences and cannot be undone.',
10911093

10921094
'settings.evolution.overview.judgeWorker.running': 'Judge: running',
10931095
'settings.evolution.overview.judgeWorker.idle': 'Judge: idle',
10941096
'settings.evolution.overview.judgeWorker.error': 'Judge: error',
1097+
'settings.evolution.overview.judgeWorker.processed': 'processed {done}/{total}',
10951098
'settings.evolution.overview.reflectionScheduler.label': 'Reflection scheduler',
10961099
'settings.evolution.overview.reflectionScheduler.never': 'never',
10971100
'settings.evolution.overview.reflectionScheduler.intervalMinutes':
10981101
'every {minutes} min',
1102+
'settings.evolution.overview.reflectionScheduler.nextRun': 'next run {time}',
1103+
'settings.evolution.overview.reflectionScheduler.modeAuto': 'auto (session end)',
1104+
'settings.evolution.overview.reflectionScheduler.modeManual': 'manual trigger',
10991105

11001106
'settings.evolution.modelEmpty.gotoProviders': 'Go to provider settings',
11011107

@@ -2026,6 +2032,9 @@ export const en = {
20262032
'chat.forkChat': 'Fork Chat',
20272033
'chat.copyMessage': 'Copy Message',
20282034
'chat.copyMessageToast': 'Message copied',
2035+
'chat.thumbUp': 'Helpful',
2036+
'chat.thumbDown': 'Not helpful',
2037+
'chat.thumbRecordedToast': 'Feedback recorded — thanks for helping the assistant evolve',
20292038
'chat.copyFailedToast': 'Copy failed',
20302039
'chat.forkChatToast': 'New agent opened with the same workspace',
20312040

0 commit comments

Comments
 (0)