@swarmclawai/swarmclaw 0.6.0 → 0.6.3
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/README.md +56 -42
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +16 -35
- package/src/app/api/tts/stream/route.ts +14 -42
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +76 -24
- package/src/components/chat/chat-header.tsx +522 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +113 -8
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +84 -17
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +125 -14
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +594 -16
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +182 -8
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +583 -63
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/file.ts +26 -7
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +118 -8
- package/src/lib/server/stream-agent-chat.ts +39 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -9,6 +9,58 @@ import { spawnSync } from 'child_process'
|
|
|
9
9
|
import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
|
|
10
10
|
import { getSearchProvider } from './search-providers'
|
|
11
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Search result compression — summarize verbose results before injecting into context
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
async function compressSearchResults(
|
|
17
|
+
results: Array<{ title?: string; url?: string; snippet?: string }>,
|
|
18
|
+
query: string,
|
|
19
|
+
bctx: ToolBuildContext,
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
const session = bctx.resolveCurrentSession?.()
|
|
22
|
+
if (!session?.provider || !session?.model) return null
|
|
23
|
+
|
|
24
|
+
const { getProvider } = await import('@/lib/providers')
|
|
25
|
+
const { loadCredentials, decryptKey } = await import('../storage')
|
|
26
|
+
const providerEntry = getProvider(session.provider)
|
|
27
|
+
if (!providerEntry?.handler?.streamChat) return null
|
|
28
|
+
|
|
29
|
+
// Resolve API key
|
|
30
|
+
let apiKey: string | undefined
|
|
31
|
+
if (session.credentialId) {
|
|
32
|
+
const creds = loadCredentials()
|
|
33
|
+
const cred = creds[session.credentialId]
|
|
34
|
+
if (cred) apiKey = decryptKey(cred)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const systemPrompt = 'You are a search result summarizer. Condense search results into a concise reference. Keep key facts, URLs, and data points. Remove filler and redundancy. Output plain text, not JSON.'
|
|
38
|
+
const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
|
|
39
|
+
|
|
40
|
+
let compressed = ''
|
|
41
|
+
await providerEntry.handler.streamChat({
|
|
42
|
+
session: { ...session, messages: [] },
|
|
43
|
+
message,
|
|
44
|
+
apiKey,
|
|
45
|
+
systemPrompt,
|
|
46
|
+
write: (raw: string) => {
|
|
47
|
+
// Extract text data from SSE lines
|
|
48
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.startsWith('data: ')) continue
|
|
51
|
+
try {
|
|
52
|
+
const ev = JSON.parse(line.slice(6))
|
|
53
|
+
if (ev.t === 'd' && ev.text) compressed += ev.text
|
|
54
|
+
} catch { /* skip */ }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
active: new Map(),
|
|
58
|
+
loadHistory: () => [],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return compressed.trim() || null
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
// ---------------------------------------------------------------------------
|
|
13
65
|
// Global registry of active browser instances for cleanup sweeps
|
|
14
66
|
// ---------------------------------------------------------------------------
|
|
@@ -70,9 +122,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
70
122
|
const settings = loadSettings()
|
|
71
123
|
const provider = await getSearchProvider(settings)
|
|
72
124
|
const results = await provider.search(query, limit)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
if (results.length === 0) return 'No results found.'
|
|
126
|
+
const raw = JSON.stringify(results, null, 2)
|
|
127
|
+
// Compress search results if they exceed 2000 chars
|
|
128
|
+
if (raw.length > 2000) {
|
|
129
|
+
try {
|
|
130
|
+
const compressed = await compressSearchResults(results, query, bctx)
|
|
131
|
+
if (compressed) return compressed
|
|
132
|
+
} catch {
|
|
133
|
+
// Compression failed — fall through to raw results
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return raw
|
|
76
137
|
} catch (err: unknown) {
|
|
77
138
|
return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
|
|
78
139
|
}
|
|
@@ -217,10 +278,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
217
278
|
|
|
218
279
|
if (Array.isArray(content)) {
|
|
219
280
|
const parts: string[] = []
|
|
220
|
-
|
|
281
|
+
const contentHasBinaryImage = content.some((c) => c.type === 'image' && !!c.data)
|
|
221
282
|
for (const c of content) {
|
|
222
283
|
if (c.type === 'image' && c.data) {
|
|
223
|
-
hasBinaryImage = true
|
|
224
284
|
const imageBuffer = Buffer.from(c.data, 'base64')
|
|
225
285
|
const filename = `screenshot-${Date.now()}.png`
|
|
226
286
|
const filepath = path.join(UPLOAD_DIR, filename)
|
|
@@ -245,8 +305,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
245
305
|
if (fs.existsSync(srcPath)) {
|
|
246
306
|
const ext = path.extname(srcPath).slice(1).toLowerCase()
|
|
247
307
|
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
|
|
248
|
-
// Skip file-path images
|
|
249
|
-
if (IMAGE_EXTS.includes(ext) &&
|
|
308
|
+
// Skip file-path images whenever MCP already returned image binary payloads.
|
|
309
|
+
if (IMAGE_EXTS.includes(ext) && contentHasBinaryImage) {
|
|
250
310
|
parts.push(isError ? text : cleanPlaywrightOutput(text))
|
|
251
311
|
} else {
|
|
252
312
|
const filename = `browser-${Date.now()}.${ext}`
|
|
@@ -284,6 +344,49 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
284
344
|
return JSON.stringify(result)
|
|
285
345
|
}
|
|
286
346
|
|
|
347
|
+
// Best-effort cookie/consent banner dismissal after navigation
|
|
348
|
+
const dismissCookieBanners = async (
|
|
349
|
+
mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>,
|
|
350
|
+
) => {
|
|
351
|
+
// Wait briefly for consent overlays to appear
|
|
352
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
353
|
+
const js = `
|
|
354
|
+
(() => {
|
|
355
|
+
const sel = [
|
|
356
|
+
// Common "Reject" / "Reject all" / "Decline" buttons
|
|
357
|
+
'button[id*="reject" i]', 'button[class*="reject" i]',
|
|
358
|
+
'a[id*="reject" i]', 'a[class*="reject" i]',
|
|
359
|
+
'[data-testid*="reject" i]', '[data-action="reject"]',
|
|
360
|
+
// OneTrust
|
|
361
|
+
'#onetrust-reject-all-handler',
|
|
362
|
+
// Cookiebot
|
|
363
|
+
'#CybotCookiebotDialogBodyButtonDecline',
|
|
364
|
+
// Didomi
|
|
365
|
+
'#didomi-notice-disagree-button',
|
|
366
|
+
// Quantcast / IAB TCF
|
|
367
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
368
|
+
'button.sp_choice_type_12',
|
|
369
|
+
// Generic patterns
|
|
370
|
+
'button[aria-label*="reject" i]', 'button[aria-label*="decline" i]',
|
|
371
|
+
'button[aria-label*="deny" i]', 'button[aria-label*="refuse" i]',
|
|
372
|
+
];
|
|
373
|
+
for (const s of sel) {
|
|
374
|
+
const el = document.querySelector(s);
|
|
375
|
+
if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; }
|
|
376
|
+
}
|
|
377
|
+
// Fallback: find buttons by visible text
|
|
378
|
+
const btns = [...document.querySelectorAll('button, a[role="button"], [class*="cookie"] button, [class*="consent"] button, [id*="cookie"] button')];
|
|
379
|
+
const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
|
|
380
|
+
for (const b of btns) {
|
|
381
|
+
const txt = (b.textContent || '').trim();
|
|
382
|
+
if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; }
|
|
383
|
+
}
|
|
384
|
+
return 'none';
|
|
385
|
+
})()
|
|
386
|
+
`
|
|
387
|
+
await mcpCall('browser_evaluate', { expression: js })
|
|
388
|
+
}
|
|
389
|
+
|
|
287
390
|
// Action-to-MCP tool mapping
|
|
288
391
|
const MCP_TOOL_MAP: Record<string, string> = {
|
|
289
392
|
navigate: 'browser_navigate',
|
|
@@ -315,7 +418,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
315
418
|
const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
|
|
316
419
|
? params.saveTo.trim()
|
|
317
420
|
: undefined
|
|
318
|
-
|
|
421
|
+
const result = await callMcpTool(mcpTool, args, { saveTo })
|
|
422
|
+
|
|
423
|
+
// After navigation, attempt to dismiss cookie consent banners
|
|
424
|
+
if (action === 'navigate') {
|
|
425
|
+
try { await dismissCookieBanners(callMcpTool) } catch { /* best-effort */ }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return result
|
|
319
429
|
} catch (err: unknown) {
|
|
320
430
|
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
321
431
|
}
|
|
@@ -9,6 +9,7 @@ import { getPluginManager } from './plugins'
|
|
|
9
9
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
|
|
10
10
|
import { getMemoryDb } from './memory-db'
|
|
11
11
|
import { logExecution } from './execution-log'
|
|
12
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
12
13
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
14
|
import { extractSuggestions } from './suggestions'
|
|
14
15
|
|
|
@@ -126,6 +127,12 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
126
127
|
opts.enabledTools.includes('manage_connectors')
|
|
127
128
|
? 'If the user wants proactive outreach (e.g., WhatsApp updates), configure connectors and pair with schedules/tasks to deliver status updates.'
|
|
128
129
|
: '',
|
|
130
|
+
opts.enabledTools.includes('manage_connectors')
|
|
131
|
+
? 'Autonomous outreach is allowed for significant events (completed/failed tasks, blockers, deadlines, meaningful reminders from memory). Avoid casual or repetitive check-ins.'
|
|
132
|
+
: '',
|
|
133
|
+
opts.enabledTools.includes('manage_connectors')
|
|
134
|
+
? 'When you proactively message through connectors, keep it concise and purposeful, and avoid sending duplicate updates about the same event.'
|
|
135
|
+
: '',
|
|
129
136
|
opts.enabledTools.includes('manage_sessions')
|
|
130
137
|
? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
|
|
131
138
|
: '',
|
|
@@ -163,6 +170,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
163
170
|
'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
|
|
164
171
|
'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
|
|
165
172
|
'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
|
|
173
|
+
'Do not end every response with a question. Use declarative completion statements by default, and only ask a question when a concrete missing detail blocks the next action.',
|
|
166
174
|
'Do not pause for a "continue" confirmation after the user has already asked you to execute a goal. Keep moving until blocked by permissions, missing credentials, or hard tool failures.',
|
|
167
175
|
'Never repeat one-time side effects that are already complete (for example creating the same schedule/task again). Verify state first, then either continue execution or reply HEARTBEAT_OK.',
|
|
168
176
|
'For main-loop tick messages that begin with "SWARM_MAIN_MISSION_TICK" or "SWARM_MAIN_AUTO_FOLLOWUP", follow that response contract exactly and include one valid [MAIN_LOOP_META] JSON line when you are not returning HEARTBEAT_OK.',
|
|
@@ -227,6 +235,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
227
235
|
stateModifierParts.push(systemPrompt!.trim())
|
|
228
236
|
} else {
|
|
229
237
|
if (settings.userPrompt) stateModifierParts.push(settings.userPrompt)
|
|
238
|
+
stateModifierParts.push(buildCurrentDateTimePromptContext())
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
// Load agent context when a full prompt was not already composed by the route layer.
|
|
@@ -400,15 +409,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
400
409
|
}
|
|
401
410
|
}
|
|
402
411
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
if (settings.suggestionsEnabled !== false) {
|
|
413
|
+
stateModifierParts.push(
|
|
414
|
+
[
|
|
415
|
+
'## Follow-up Suggestions',
|
|
416
|
+
'At the end of every response, include a <suggestions> block with exactly 3 short',
|
|
417
|
+
'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
|
|
418
|
+
'Make them contextual to what you just said. Example:',
|
|
419
|
+
'<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
|
|
420
|
+
].join('\n'),
|
|
421
|
+
)
|
|
422
|
+
}
|
|
412
423
|
|
|
413
424
|
stateModifierParts.push(
|
|
414
425
|
buildAgenticExecutionPolicy({
|
|
@@ -548,8 +559,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
548
559
|
// Context manager failure — continue with full history
|
|
549
560
|
}
|
|
550
561
|
|
|
562
|
+
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
563
|
+
let contextStart = 0
|
|
564
|
+
for (let i = effectiveHistory.length - 1; i >= 0; i--) {
|
|
565
|
+
if (effectiveHistory[i].kind === 'context-clear') {
|
|
566
|
+
contextStart = i + 1
|
|
567
|
+
break
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
571
|
+
|
|
551
572
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
552
|
-
for (const m of
|
|
573
|
+
for (const m of postClearHistory.slice(-20)) {
|
|
553
574
|
if (m.role === 'user') {
|
|
554
575
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
555
576
|
} else {
|
|
@@ -567,6 +588,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
567
588
|
let totalInputTokens = 0
|
|
568
589
|
let totalOutputTokens = 0
|
|
569
590
|
let lastToolInput: unknown = null
|
|
591
|
+
let accumulatedThinking = ''
|
|
570
592
|
|
|
571
593
|
// Plugin hooks: beforeAgentStart
|
|
572
594
|
const pluginMgr = getPluginManager()
|
|
@@ -603,9 +625,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
603
625
|
for (const block of chunk.content) {
|
|
604
626
|
// Anthropic extended thinking blocks
|
|
605
627
|
if (block.type === 'thinking' && block.thinking) {
|
|
628
|
+
accumulatedThinking += block.thinking
|
|
606
629
|
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
607
630
|
// OpenClaw [[thinking]] prefix convention
|
|
608
631
|
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
632
|
+
accumulatedThinking += block.text.slice(12)
|
|
609
633
|
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
610
634
|
} else if (block.text) {
|
|
611
635
|
fullText += block.text
|
|
@@ -730,6 +754,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
730
754
|
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
|
|
731
755
|
}
|
|
732
756
|
|
|
757
|
+
// Emit full thinking text as metadata so the client can persist it
|
|
758
|
+
if (accumulatedThinking) {
|
|
759
|
+
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ thinking: accumulatedThinking }) })}\n\n`)
|
|
760
|
+
}
|
|
761
|
+
|
|
733
762
|
// Track cost
|
|
734
763
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
735
764
|
if (totalTokens > 0) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse @AgentName mentions from text and resolve to an agent ID.
|
|
5
|
+
* Uses case-insensitive exact match, then falls back to starts-with.
|
|
6
|
+
*/
|
|
7
|
+
export function parseMentionedAgentId(
|
|
8
|
+
description: string,
|
|
9
|
+
agents: Record<string, Agent>,
|
|
10
|
+
): string | null {
|
|
11
|
+
const mentionRegex = /@(\S+)/g
|
|
12
|
+
const agentList = Object.values(agents)
|
|
13
|
+
let match: RegExpExecArray | null
|
|
14
|
+
|
|
15
|
+
while ((match = mentionRegex.exec(description)) !== null) {
|
|
16
|
+
const mention = match[1].toLowerCase()
|
|
17
|
+
|
|
18
|
+
// Exact name match (case-insensitive)
|
|
19
|
+
const exact = agentList.find((a) => a.name.toLowerCase() === mention)
|
|
20
|
+
if (exact) return exact.id
|
|
21
|
+
|
|
22
|
+
// Starts-with match (for partial names like @code matching "CodeBot")
|
|
23
|
+
const startsWith = agentList.find((a) => a.name.toLowerCase().startsWith(mention))
|
|
24
|
+
if (startsWith) return startsWith.id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve task agent: if description has an @mention, use that agent.
|
|
32
|
+
* Otherwise fall back to currentAgentId.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveTaskAgentFromDescription(
|
|
35
|
+
description: string,
|
|
36
|
+
currentAgentId: string,
|
|
37
|
+
agents: Record<string, Agent>,
|
|
38
|
+
): string {
|
|
39
|
+
const mentioned = parseMentionedAgentId(description, agents)
|
|
40
|
+
return mentioned || currentAgentId
|
|
41
|
+
}
|
package/src/lib/sessions.ts
CHANGED
|
@@ -34,6 +34,16 @@ export const deleteSession = (id: string) =>
|
|
|
34
34
|
export const fetchMessages = (id: string) =>
|
|
35
35
|
api<Message[]>('GET', `/sessions/${id}/messages`)
|
|
36
36
|
|
|
37
|
+
export interface PaginatedMessages {
|
|
38
|
+
messages: Message[]
|
|
39
|
+
total: number
|
|
40
|
+
hasMore: boolean
|
|
41
|
+
startIndex: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const fetchMessagesPaginated = (id: string, limit: number = 100) =>
|
|
45
|
+
api<PaginatedMessages>('GET', `/sessions/${id}/messages?limit=${limit}`)
|
|
46
|
+
|
|
37
47
|
export const clearMessages = (id: string) =>
|
|
38
48
|
api<string>('POST', `/sessions/${id}/clear`)
|
|
39
49
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface SoulTemplate {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
description: string
|
|
5
|
+
soul: string
|
|
6
|
+
tags: string[]
|
|
7
|
+
archetype: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const SOUL_ARCHETYPES = [
|
|
11
|
+
'All',
|
|
12
|
+
'Engineer',
|
|
13
|
+
'Mentor',
|
|
14
|
+
'Creative',
|
|
15
|
+
'Analyst',
|
|
16
|
+
'Leader',
|
|
17
|
+
'Researcher',
|
|
18
|
+
'Communicator',
|
|
19
|
+
'Operator',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
export type SoulArchetype = (typeof SOUL_ARCHETYPES)[number]
|
|
23
|
+
|
|
24
|
+
export const SOUL_LIBRARY: SoulTemplate[] = [
|
|
25
|
+
// --- Engineer ---
|
|
26
|
+
{ id: 'eng-01', name: 'The Pragmatist', description: 'Practical, no-nonsense engineer who ships.', soul: 'You are pragmatic to the core. You prefer "good enough now" over "perfect someday." Every suggestion comes with a concrete next step.', tags: ['practical', 'direct', 'shipping'], archetype: 'Engineer' },
|
|
27
|
+
{ id: 'eng-02', name: 'Systems Thinker', description: 'Zooms out to see architecture and trade-offs.', soul: 'You think like a systems designer. You always zoom out to see the bigger picture. Every solution has a cost, and you name it.', tags: ['architecture', 'trade-offs', 'big-picture'], archetype: 'Engineer' },
|
|
28
|
+
{ id: 'eng-03', name: 'The Hacker', description: 'Clever shortcuts and unconventional solutions.', soul: 'You have a hacker mentality. You love finding clever shortcuts and unconventional solutions. You are scrappy and resourceful.', tags: ['creative', 'resourceful', 'unconventional'], archetype: 'Engineer' },
|
|
29
|
+
{ id: 'eng-04', name: 'Detail Hunter', description: 'Catches edge cases everyone else misses.', soul: 'You are detail-oriented to a fault. You catch edge cases everyone else misses. You are meticulous and treat every detail as if it matters.', tags: ['thorough', 'edge-cases', 'precise'], archetype: 'Engineer' },
|
|
30
|
+
{ id: 'eng-05', name: 'The Craftsperson', description: 'Takes pride in clean, elegant code.', soul: 'You speak like a craftsperson — you care about the details because you take pride in the work. You are enthusiastic about elegance.', tags: ['quality', 'elegant', 'pride'], archetype: 'Engineer' },
|
|
31
|
+
{ id: 'eng-06', name: 'Prototyper', description: 'Builds first, specs later.', soul: 'You are practical and hands-on. You\'d rather build a prototype than write a spec. You have a tinkerer\'s spirit and love iterating.', tags: ['prototyping', 'hands-on', 'iterative'], archetype: 'Engineer' },
|
|
32
|
+
{ id: 'eng-07', name: 'The Minimalist', description: 'Least code, most impact.', soul: 'You are minimalist in communication. You say what needs to be said and nothing more. You value simplicity and clarity above all.', tags: ['concise', 'minimal', 'clean'], archetype: 'Engineer' },
|
|
33
|
+
{ id: 'eng-08', name: 'Seasoned Veteran', description: 'Calm authority from years of experience.', soul: 'You speak like a seasoned engineer — no buzzwords, just clear technical communication. You speak with the calm authority of someone who has seen it all.', tags: ['experienced', 'calm', 'no-buzzwords'], archetype: 'Engineer' },
|
|
34
|
+
|
|
35
|
+
// --- Mentor ---
|
|
36
|
+
{ id: 'men-01', name: 'Patient Teacher', description: 'Explains complex things simply.', soul: 'You speak like a patient mentor. You explain complex things using simple analogies. You never make someone feel bad for not knowing something.', tags: ['patient', 'analogies', 'supportive'], archetype: 'Mentor' },
|
|
37
|
+
{ id: 'men-02', name: 'Socratic Guide', description: 'Leads through questions, not answers.', soul: 'You have a gentle, Socratic style. You guide through questions rather than giving direct answers. You help people discover solutions themselves.', tags: ['questions', 'discovery', 'gentle'], archetype: 'Mentor' },
|
|
38
|
+
{ id: 'men-03', name: 'The Coach', description: 'Pushes you to be better while having your back.', soul: 'You have a coach\'s mindset. You push people to be better while making them feel supported. You are nurturing but don\'t sugarcoat hard truths.', tags: ['growth', 'supportive', 'challenging'], archetype: 'Mentor' },
|
|
39
|
+
{ id: 'men-04', name: 'Warm Encourager', description: 'Finds the positive before the constructive.', soul: 'You are warm and encouraging, always finding something positive to highlight before giving constructive feedback. You lead with empathy.', tags: ['positive', 'empathetic', 'encouraging'], archetype: 'Mentor' },
|
|
40
|
+
{ id: 'men-05', name: 'Knowledge Sharer', description: 'Teaches as they work.', soul: 'You are generous with your knowledge. You teach as you work. You treat every conversation as a chance to help someone learn.', tags: ['teaching', 'generous', 'collaborative'], archetype: 'Mentor' },
|
|
41
|
+
|
|
42
|
+
// --- Creative ---
|
|
43
|
+
{ id: 'cre-01', name: 'The Storyteller', description: 'Explains through narratives and examples.', soul: 'You are a storyteller. You explain concepts through narratives and real-world examples. You make abstract ideas tangible and memorable.', tags: ['narrative', 'examples', 'engaging'], archetype: 'Creative' },
|
|
44
|
+
{ id: 'cre-02', name: 'Lateral Thinker', description: 'Approaches problems from unexpected angles.', soul: 'You are a creative thinker. You approach problems from unexpected angles. You are a connector who notices patterns across domains.', tags: ['creative', 'unexpected', 'cross-domain'], archetype: 'Creative' },
|
|
45
|
+
{ id: 'cre-03', name: 'The Explorer', description: 'Loves venturing into unfamiliar territory.', soul: 'You have an explorer\'s curiosity. You love venturing into unfamiliar territory. You are naturally curious and stubbornly persistent in understanding.', tags: ['curious', 'adventurous', 'persistent'], archetype: 'Creative' },
|
|
46
|
+
{ id: 'cre-04', name: 'Playful Inventor', description: 'Loves "what if" questions and edge cases.', soul: 'You have a playful, curious personality. You love asking "what if" questions and exploring edge cases. You are whimsical but know when to be serious.', tags: ['playful', 'curious', 'inventive'], archetype: 'Creative' },
|
|
47
|
+
{ id: 'cre-05', name: 'The Poet', description: 'Chooses words that resonate.', soul: 'You have a poet\'s sensitivity to language. You choose words that resonate. You have a designer\'s eye and care about how things feel.', tags: ['language', 'aesthetic', 'thoughtful'], archetype: 'Creative' },
|
|
48
|
+
|
|
49
|
+
// --- Analyst ---
|
|
50
|
+
{ id: 'ana-01', name: 'Data-Driven', description: 'Always backs claims with numbers.', soul: 'You are data-driven. You always back claims with numbers, benchmarks, or citations. You have a scientist\'s rigor — hypothesize, test, revise.', tags: ['data', 'evidence', 'rigorous'], archetype: 'Analyst' },
|
|
51
|
+
{ id: 'ana-02', name: 'The Skeptic', description: 'Challenges assumptions and demands evidence.', soul: 'You are skeptical by nature. You challenge assumptions and ask for evidence. You are a devil\'s advocate who stress-tests ideas.', tags: ['skeptical', 'critical', 'thorough'], archetype: 'Analyst' },
|
|
52
|
+
{ id: 'ana-03', name: 'Methodical Planner', description: 'Considers what could go wrong first.', soul: 'You are methodical and thorough. You always consider what could go wrong before recommending a path forward. You break complex problems into numbered steps.', tags: ['methodical', 'risk-aware', 'structured'], archetype: 'Analyst' },
|
|
53
|
+
{ id: 'ana-04', name: 'The Economist', description: 'Thinks in incentives and trade-offs.', soul: 'You think like an economist — always considering incentives, trade-offs, and unintended consequences. You name the cost of every solution.', tags: ['trade-offs', 'incentives', 'strategic'], archetype: 'Analyst' },
|
|
54
|
+
{ id: 'ana-05', name: 'Pattern Spotter', description: 'Notices subtle signals others miss.', soul: 'You have a naturalist\'s attention to patterns. You notice subtle signals others miss. You are observant and perceptive.', tags: ['patterns', 'observant', 'insight'], archetype: 'Analyst' },
|
|
55
|
+
|
|
56
|
+
// --- Leader ---
|
|
57
|
+
{ id: 'lea-01', name: 'Decisive Commander', description: 'Gathers info, then acts.', soul: 'You are decisive. You gather enough information to act, then act. You communicate with military precision — clear, structured, decisive.', tags: ['decisive', 'structured', 'action'], archetype: 'Leader' },
|
|
58
|
+
{ id: 'lea-02', name: 'Bold Visionary', description: 'Takes clear stances and defends them.', soul: 'You are bold and opinionated. You take clear stances and defend them with reasoning. You have an infectious optimism.', tags: ['bold', 'opinionated', 'optimistic'], archetype: 'Leader' },
|
|
59
|
+
{ id: 'lea-03', name: 'Calm Under Fire', description: 'The bigger the problem, the more composed.', soul: 'You are calm under pressure. The bigger the problem, the more composed you become. You have a zen-like calm that simplifies complexity.', tags: ['calm', 'composed', 'resilient'], archetype: 'Leader' },
|
|
60
|
+
{ id: 'lea-04', name: 'The Diplomat', description: 'Presents all perspectives before their own.', soul: 'You are diplomatic and measured. You present multiple perspectives before offering your own. You are collaborative and build on others\' ideas.', tags: ['diplomatic', 'balanced', 'collaborative'], archetype: 'Leader' },
|
|
61
|
+
{ id: 'lea-05', name: 'The Strategist', description: 'Always thinking two steps ahead.', soul: 'You are strategic. You always think two steps ahead. You are fiercely independent in your thinking and form opinions from first principles.', tags: ['strategic', 'forward-thinking', 'principled'], archetype: 'Leader' },
|
|
62
|
+
|
|
63
|
+
// --- Researcher ---
|
|
64
|
+
{ id: 'res-01', name: 'The Academic', description: 'Precise, well-cited, and thorough.', soul: 'You have an academic tone — precise, well-cited, and thorough. You qualify your claims carefully and admit uncertainty openly.', tags: ['academic', 'precise', 'cited'], archetype: 'Researcher' },
|
|
65
|
+
{ id: 'res-02', name: 'Deep Diver', description: 'Keeps digging until truly understanding.', soul: 'You are stubbornly curious. You keep digging until you truly understand. You are a deep thinker who surfaces insights others miss.', tags: ['deep', 'curious', 'insightful'], archetype: 'Researcher' },
|
|
66
|
+
{ id: 'res-03', name: 'The Investigator', description: 'Probing questions to get to the real story.', soul: 'You have a journalist\'s instinct. You ask probing questions to get to the real story. You are naturally inquisitive and persistent.', tags: ['probing', 'investigative', 'thorough'], archetype: 'Researcher' },
|
|
67
|
+
{ id: 'res-04', name: 'Think-Aloud Reasoner', description: 'Walks through reasoning step by step.', soul: 'You think out loud, walking through your reasoning step by step. You admit uncertainty openly and revise your thinking as new evidence appears.', tags: ['transparent', 'step-by-step', 'honest'], archetype: 'Researcher' },
|
|
68
|
+
|
|
69
|
+
// --- Communicator ---
|
|
70
|
+
{ id: 'com-01', name: 'Straight Shooter', description: 'Says exactly what they mean.', soul: 'You are a straight shooter. You say exactly what you mean without hedging. You are blunt and efficient — no fluff, no pleasantries.', tags: ['direct', 'blunt', 'honest'], archetype: 'Communicator' },
|
|
71
|
+
{ id: 'com-02', name: 'Dry Wit', description: 'Sharp humor that catches you off guard.', soul: 'You have a dry, deadpan delivery. Your humor catches people off guard. You make sharp observations but never at someone\'s expense.', tags: ['witty', 'dry', 'clever'], archetype: 'Communicator' },
|
|
72
|
+
{ id: 'com-03', name: 'Coffee Chat', description: 'Casual, approachable, like talking to a friend.', soul: 'You are casual and approachable. You write like you\'re talking to a friend over coffee. You are lighthearted and fun but take work seriously.', tags: ['casual', 'approachable', 'friendly'], archetype: 'Communicator' },
|
|
73
|
+
{ id: 'com-04', name: 'Precise Wordsmith', description: 'Every word chosen deliberately.', soul: 'You speak with precision. You choose every word deliberately and avoid ambiguity. You are crisp and formal with clear structure.', tags: ['precise', 'formal', 'structured'], archetype: 'Communicator' },
|
|
74
|
+
{ id: 'com-05', name: 'Warm & Direct', description: 'Kindness meets candor.', soul: 'You are warm but direct. You combine kindness with candor effortlessly. You are kind but not soft — you hold high standards with a warm touch.', tags: ['warm', 'candid', 'balanced'], archetype: 'Communicator' },
|
|
75
|
+
{ id: 'com-06', name: 'The Entertainer', description: 'Makes technical topics fun.', soul: 'You are witty and quick. You make technical topics entertaining without dumbing them down. You are energetic and genuinely excited about clever solutions.', tags: ['entertaining', 'energetic', 'witty'], archetype: 'Communicator' },
|
|
76
|
+
|
|
77
|
+
// --- Operator ---
|
|
78
|
+
{ id: 'ops-01', name: 'Reliable Executor', description: 'Under-promises, over-delivers.', soul: 'You are reliable and steady. You under-promise and over-deliver. You are action-oriented and bias toward doing over discussing.', tags: ['reliable', 'action', 'steady'], archetype: 'Operator' },
|
|
79
|
+
{ id: 'ops-02', name: 'The Adapter', description: 'Matches style to the situation.', soul: 'You are adaptable. You match your communication style to what the situation needs. You are efficient and no-nonsense but make time for the human side.', tags: ['adaptable', 'flexible', 'situational'], archetype: 'Operator' },
|
|
80
|
+
{ id: 'ops-03', name: 'Problem Solver', description: 'Sees obstacles as puzzles to crack.', soul: 'You are a problem solver at heart. You see obstacles as puzzles to crack. You make the most of whatever you have and never give up easily.', tags: ['problem-solving', 'persistent', 'resourceful'], archetype: 'Operator' },
|
|
81
|
+
{ id: 'ops-04', name: 'Gardener', description: 'Nurtures ideas and lets them grow.', soul: 'You have a gardener\'s patience. You nurture ideas and let them grow. You are gently persistent — you don\'t give up easily but never push too hard.', tags: ['patient', 'nurturing', 'organic'], archetype: 'Operator' },
|
|
82
|
+
{ id: 'ops-05', name: 'Quiet Confidence', description: 'Nothing to prove, everything to offer.', soul: 'You communicate with quiet confidence. You prefer showing over telling. You speak with the easy confidence of someone who has nothing to prove.', tags: ['confident', 'understated', 'authentic'], archetype: 'Operator' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
/** Search souls by query text and optional archetype filter. */
|
|
86
|
+
export function searchSouls(query: string, archetype?: string): SoulTemplate[] {
|
|
87
|
+
const q = query.toLowerCase().trim()
|
|
88
|
+
let results = SOUL_LIBRARY
|
|
89
|
+
|
|
90
|
+
if (archetype && archetype !== 'All') {
|
|
91
|
+
results = results.filter((s) => s.archetype === archetype)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!q) return results
|
|
95
|
+
|
|
96
|
+
return results.filter(
|
|
97
|
+
(s) =>
|
|
98
|
+
s.name.toLowerCase().includes(q) ||
|
|
99
|
+
s.description.toLowerCase().includes(q) ||
|
|
100
|
+
s.tags.some((t) => t.includes(q)) ||
|
|
101
|
+
s.soul.toLowerCase().includes(q),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import type { BoardTask } from '@/types'
|
|
3
|
+
|
|
4
|
+
/** SHA-256 fingerprint from title + agentId, first 16 hex chars. */
|
|
5
|
+
export function computeTaskFingerprint(title: string, agentId: string): string {
|
|
6
|
+
const input = `${title.trim().toLowerCase()}::${agentId}`
|
|
7
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TERMINAL_STATUSES = new Set(['completed', 'archived', 'failed'])
|
|
11
|
+
|
|
12
|
+
/** Find an existing non-terminal task with the same fingerprint. */
|
|
13
|
+
export function findDuplicateTask(
|
|
14
|
+
tasks: Record<string, BoardTask>,
|
|
15
|
+
candidate: { fingerprint: string },
|
|
16
|
+
): BoardTask | null {
|
|
17
|
+
for (const task of Object.values(tasks)) {
|
|
18
|
+
if (
|
|
19
|
+
task.fingerprint === candidate.fingerprint &&
|
|
20
|
+
!TERMINAL_STATUSES.has(task.status)
|
|
21
|
+
) {
|
|
22
|
+
return task
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
@@ -22,6 +22,8 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
|
|
|
22
22
|
{ id: 'sandbox', label: 'Sandbox', description: 'Run JS/TS/Python code in an isolated Deno sandbox' },
|
|
23
23
|
{ id: 'create_document', label: 'Create Document', description: 'Render markdown to PDF, HTML, or image' },
|
|
24
24
|
{ id: 'create_spreadsheet', label: 'Create Spreadsheet', description: 'Create Excel or CSV files from structured data' },
|
|
25
|
+
{ id: 'http_request', label: 'HTTP Request', description: 'Make HTTP API calls (GET, POST, PUT, DELETE, etc.)' },
|
|
26
|
+
{ id: 'git', label: 'Git', description: 'Run structured git operations (status, commit, push, diff, etc.)' },
|
|
25
27
|
]
|
|
26
28
|
|
|
27
29
|
export const PLATFORM_TOOLS: ToolDefinition[] = [
|
package/src/lib/tts.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function initAudioContext() {
|
|
|
10
10
|
ensureContext()
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function speak(text: string) {
|
|
13
|
+
export async function speak(text: string, voiceId?: string | null) {
|
|
14
14
|
if (currentSource) {
|
|
15
15
|
try { currentSource.stop() } catch { /* noop */ }
|
|
16
16
|
currentSource = null
|
|
@@ -21,7 +21,7 @@ export async function speak(text: string) {
|
|
|
21
21
|
const res = await fetch('/api/tts', {
|
|
22
22
|
method: 'POST',
|
|
23
23
|
headers: { 'Content-Type': 'application/json' },
|
|
24
|
-
body: JSON.stringify({ text: text.slice(0, 2000) }),
|
|
24
|
+
body: JSON.stringify({ text: text.slice(0, 2000), ...(voiceId ? { voiceId } : {}) }),
|
|
25
25
|
})
|
|
26
26
|
if (!res.ok) return
|
|
27
27
|
|
|
@@ -98,6 +98,8 @@ interface AppState {
|
|
|
98
98
|
setTaskSheetOpen: (open: boolean) => void
|
|
99
99
|
editingTaskId: string | null
|
|
100
100
|
setEditingTaskId: (id: string | null) => void
|
|
101
|
+
taskSheetViewOnly: boolean
|
|
102
|
+
setTaskSheetViewOnly: (v: boolean) => void
|
|
101
103
|
|
|
102
104
|
// Provider configs (custom providers)
|
|
103
105
|
providerConfigs: ProviderConfig[]
|
|
@@ -460,9 +462,11 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
460
462
|
get().loadTasks(show)
|
|
461
463
|
},
|
|
462
464
|
taskSheetOpen: false,
|
|
463
|
-
setTaskSheetOpen: (open) => set({ taskSheetOpen: open }),
|
|
465
|
+
setTaskSheetOpen: (open) => set({ taskSheetOpen: open, ...(open ? {} : { taskSheetViewOnly: false }) }),
|
|
464
466
|
editingTaskId: null,
|
|
465
467
|
setEditingTaskId: (id) => set({ editingTaskId: id }),
|
|
468
|
+
taskSheetViewOnly: false,
|
|
469
|
+
setTaskSheetViewOnly: (v) => set({ taskSheetViewOnly: v }),
|
|
466
470
|
|
|
467
471
|
// Provider configs (custom providers)
|
|
468
472
|
providerConfigs: [],
|