@swarmclawai/swarmclaw 1.5.67 → 1.5.69
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 +18 -0
- package/package.json +14 -7
- package/src/app/api/runs/[id]/events/route.ts +10 -4
- package/src/app/api/runs/[id]/route.ts +6 -2
- package/src/app/api/runs/route.test.ts +84 -0
- package/src/app/api/runs/route.ts +41 -2
- package/src/components/agents/inspector-panel.tsx +2 -1
- package/src/components/chat/chat-area.tsx +57 -12
- package/src/components/chat/chat-header.tsx +20 -1
- package/src/components/schedules/schedule-console.tsx +148 -30
- package/src/lib/chat/new-session.test.ts +114 -0
- package/src/lib/chat/new-session.ts +146 -0
- package/src/lib/server/daemon/controller.test.ts +78 -0
- package/src/lib/server/daemon/controller.ts +50 -7
- package/src/lib/server/missions/mission-templates.test.ts +9 -0
- package/src/lib/server/missions/mission-templates.ts +23 -0
- package/src/lib/server/protocols/protocol-agent-turn.test.ts +164 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +119 -16
- package/src/lib/server/provider-endpoint.ts +0 -3
- package/src/lib/server/runs/unified-run-records.ts +91 -0
- package/src/lib/server/schedules/schedule-normalization.ts +4 -1
- package/src/lib/server/schedules/schedule-service.test.ts +73 -0
- package/src/lib/server/schedules/schedule-service.ts +10 -3
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +3 -1
package/README.md
CHANGED
|
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.69 Highlights
|
|
403
|
+
|
|
404
|
+
Fast-follow release for [#55](https://github.com/swarmclawai/swarmclaw/pull/55) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
|
|
405
|
+
|
|
406
|
+
- **Structured runs are easier to find.** Schedule-backed protocol runs now appear in the schedule console and unified `/api/runs` endpoints, including detail and event fallbacks for structured run records.
|
|
407
|
+
- **Agent sessions get a cleaner fresh-chat flow.** Agent chat headers now expose a New chat action for sessions with history or saved CLI/runtime handles, first prompts derive compact session titles, and agent session lists sort newest-first.
|
|
408
|
+
- **Structured session execution is sturdier.** CLI providers can execute structured turns through their direct provider runtime, blank structured responses now surface the real logged error where possible, successful structured turns clear their watchdog timers promptly, schedule timing changes recompute `nextRunAt`, and in-process daemon status/control paths are covered.
|
|
409
|
+
- **Package contents are safer.** The npm package allowlist now explicitly excludes local env files under `src/` even when a maintainer has private ignored config in their working tree.
|
|
410
|
+
|
|
411
|
+
### v1.5.68 Highlights
|
|
412
|
+
|
|
413
|
+
Launch-readiness release for turning SwarmClaw's own next launch into a reusable workflow.
|
|
414
|
+
|
|
415
|
+
- **Launch Week Growth Sprint mission template.** The mission template gallery now includes a launch-week operator that audits the product/docs, drafts GitHub Release, Product Hunt, Show HN, social, and community copy, identifies the top demo moments, and produces daily feedback/metrics/follow-up reports. The default goal explicitly keeps public posting behind approval.
|
|
416
|
+
- **Security and release metadata refresh.** Next.js is updated to `16.2.4` in the app and docs site, OpenClaw / Discord.js / selected transitives are refreshed so the production high/critical audit gate passes, and the stale `package-lock.json` root version is realigned with the published package version.
|
|
417
|
+
- **Desktop release gate hardening.** `npm run electron:build` now restores host-architecture native modules after macOS multi-arch packaging, so a local release smoke build no longer leaves the checkout unable to run the next host build.
|
|
418
|
+
- **Launch assets and docs.** Added a concrete v1.5.68 launch plan in `docs/release/v1.5.68-launch-plan.md`, refreshed the website release notes/docs index, and updated stale install examples so public launch traffic sees current instructions.
|
|
419
|
+
|
|
402
420
|
### v1.5.67 Highlights
|
|
403
421
|
|
|
404
422
|
Three chatroom-focused fixes from a community contribution by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.69",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
"access": "public",
|
|
14
14
|
"registry": "https://registry.npmjs.org/"
|
|
15
15
|
},
|
|
16
|
+
"overrides": {
|
|
17
|
+
"protobufjs": "7.5.5",
|
|
18
|
+
"request": {
|
|
19
|
+
"form-data": "2.5.5"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
16
22
|
"repository": {
|
|
17
23
|
"type": "git",
|
|
18
24
|
"url": "git+https://github.com/swarmclawai/swarmclaw.git"
|
|
@@ -38,6 +44,7 @@
|
|
|
38
44
|
"bin/",
|
|
39
45
|
"skills/",
|
|
40
46
|
"src/",
|
|
47
|
+
"!src/**/.env*",
|
|
41
48
|
"public/",
|
|
42
49
|
"Dockerfile.sandbox-browser",
|
|
43
50
|
"scripts/easy-setup.mjs",
|
|
@@ -100,7 +107,7 @@
|
|
|
100
107
|
"@langchain/core": "^1.1.31",
|
|
101
108
|
"@langchain/langgraph": "^1.2.2",
|
|
102
109
|
"@langchain/openai": "^1.2.8",
|
|
103
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
110
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
104
111
|
"@multiavatar/multiavatar": "^1.0.7",
|
|
105
112
|
"@opentelemetry/api": "^1.9.1",
|
|
106
113
|
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
|
@@ -130,22 +137,22 @@
|
|
|
130
137
|
"cron-parser": "^5.5.0",
|
|
131
138
|
"cronstrue": "^3.12.0",
|
|
132
139
|
"dagre": "^0.8.5",
|
|
133
|
-
"discord.js": "^14.
|
|
140
|
+
"discord.js": "^14.26.3",
|
|
141
|
+
"dompurify": "^3.4.1",
|
|
134
142
|
"electron-updater": "^6.3.9",
|
|
135
143
|
"ethers": "^6.16.0",
|
|
136
144
|
"exceljs": "^4.4.0",
|
|
137
145
|
"grammy": "^1.40.0",
|
|
138
146
|
"highlight.js": "^11.11.1",
|
|
139
|
-
"dompurify": "^3.3.3",
|
|
140
147
|
"imapflow": "^1.2.11",
|
|
141
148
|
"just-bash": "^2.14.0",
|
|
142
149
|
"langchain": "^1.2.30",
|
|
143
150
|
"lucide-react": "^0.574.0",
|
|
144
151
|
"mailparser": "^3.9.3",
|
|
145
|
-
"next": "16.
|
|
152
|
+
"next": "16.2.4",
|
|
146
153
|
"next-themes": "^0.4.6",
|
|
147
154
|
"nodemailer": "^8.0.1",
|
|
148
|
-
"openclaw": "^2026.4.
|
|
155
|
+
"openclaw": "^2026.4.22",
|
|
149
156
|
"pdf-parse": "^2.4.5",
|
|
150
157
|
"qrcode": "^1.5.4",
|
|
151
158
|
"radix-ui": "^1.4.3",
|
|
@@ -175,7 +182,7 @@
|
|
|
175
182
|
"electron": "^33.3.0",
|
|
176
183
|
"electron-builder": "^25.1.8",
|
|
177
184
|
"eslint": "^9",
|
|
178
|
-
"eslint-config-next": "16.
|
|
185
|
+
"eslint-config-next": "16.2.4",
|
|
179
186
|
"png-to-ico": "^3.0.1"
|
|
180
187
|
},
|
|
181
188
|
"optionalDependencies": {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getRunById, listRunEvents } from '@/lib/server/runtime/session-run-manager'
|
|
3
|
+
import { listProtocolRunEventsForRun, loadProtocolRunById } from '@/lib/server/protocols/protocol-queries'
|
|
4
|
+
import { protocolEventToRunEventRecord } from '@/lib/server/runs/unified-run-records'
|
|
3
5
|
|
|
4
6
|
export const dynamic = 'force-dynamic'
|
|
5
7
|
|
|
@@ -12,11 +14,15 @@ function parseLimit(value: string | null): number | undefined {
|
|
|
12
14
|
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
13
15
|
const { id } = await params
|
|
14
16
|
const run = getRunById(id)
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
+
if (run) {
|
|
18
|
+
const url = new URL(req.url)
|
|
19
|
+
const limit = parseLimit(url.searchParams.get('limit'))
|
|
20
|
+
return NextResponse.json(listRunEvents(id, limit))
|
|
17
21
|
}
|
|
18
|
-
|
|
22
|
+
const protocolRun = loadProtocolRunById(id)
|
|
23
|
+
if (!protocolRun) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
|
|
19
24
|
const url = new URL(req.url)
|
|
20
25
|
const limit = parseLimit(url.searchParams.get('limit'))
|
|
21
|
-
|
|
26
|
+
const events = listProtocolRunEventsForRun(id, limit || 200).map((event) => protocolEventToRunEventRecord(protocolRun, event))
|
|
27
|
+
return NextResponse.json(events)
|
|
22
28
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getRunById } from '@/lib/server/runtime/session-run-manager'
|
|
3
|
+
import { loadProtocolRunById } from '@/lib/server/protocols/protocol-queries'
|
|
4
|
+
import { protocolRunToSessionRunRecord } from '@/lib/server/runs/unified-run-records'
|
|
3
5
|
|
|
4
6
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
7
|
const { id } = await params
|
|
6
8
|
const run = getRunById(id)
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
+
if (run) return NextResponse.json(run)
|
|
10
|
+
const protocolRun = loadProtocolRunById(id)
|
|
11
|
+
if (protocolRun) return NextResponse.json(protocolRunToSessionRunRecord(protocolRun))
|
|
12
|
+
return NextResponse.json({ error: 'Run not found' }, { status: 404 })
|
|
9
13
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
test('runs routes include structured schedule protocol runs', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
listCount: number
|
|
9
|
+
firstRunId: string | null
|
|
10
|
+
firstRunSource: string | null
|
|
11
|
+
firstRunStatus: string | null
|
|
12
|
+
detailId: string | null
|
|
13
|
+
detailSource: string | null
|
|
14
|
+
eventsCount: number
|
|
15
|
+
eventSummary: string | null
|
|
16
|
+
}>(`
|
|
17
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
18
|
+
const protocolsMod = await import('./src/lib/server/protocols/protocol-service')
|
|
19
|
+
const listRouteMod = await import('./src/app/api/runs/route')
|
|
20
|
+
const detailRouteMod = await import('./src/app/api/runs/[id]/route')
|
|
21
|
+
const eventsRouteMod = await import('./src/app/api/runs/[id]/events/route')
|
|
22
|
+
const storage = storageMod.default || storageMod
|
|
23
|
+
const protocols = protocolsMod.default || protocolsMod
|
|
24
|
+
const listRoute = listRouteMod.default || listRouteMod
|
|
25
|
+
const detailRoute = detailRouteMod.default || detailRouteMod
|
|
26
|
+
const eventsRoute = eventsRouteMod.default || eventsRouteMod
|
|
27
|
+
|
|
28
|
+
storage.upsertStoredItem('agents', 'agentA', {
|
|
29
|
+
id: 'agentA',
|
|
30
|
+
name: 'Agent A',
|
|
31
|
+
provider: 'ollama',
|
|
32
|
+
model: 'test-model',
|
|
33
|
+
systemPrompt: 'test',
|
|
34
|
+
createdAt: 1,
|
|
35
|
+
updatedAt: 1,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const run = protocols.createProtocolRun({
|
|
39
|
+
title: 'Scheduled structured run',
|
|
40
|
+
participantAgentIds: ['agentA'],
|
|
41
|
+
facilitatorAgentId: 'agentA',
|
|
42
|
+
autoStart: false,
|
|
43
|
+
scheduleId: 'sched-1',
|
|
44
|
+
sourceRef: { kind: 'schedule', id: 'sched-1', label: 'Morning schedule' },
|
|
45
|
+
config: {
|
|
46
|
+
goal: 'Summarize the morning inbox.',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const listResponse = await listRoute.GET(new Request('http://local/api/runs?limit=10'))
|
|
51
|
+
const listPayload = await listResponse.json()
|
|
52
|
+
|
|
53
|
+
const detailResponse = await detailRoute.GET(
|
|
54
|
+
new Request('http://local/api/runs/' + run.id),
|
|
55
|
+
{ params: Promise.resolve({ id: run.id }) },
|
|
56
|
+
)
|
|
57
|
+
const detailPayload = await detailResponse.json()
|
|
58
|
+
|
|
59
|
+
const eventsResponse = await eventsRoute.GET(
|
|
60
|
+
new Request('http://local/api/runs/' + run.id + '/events?limit=10'),
|
|
61
|
+
{ params: Promise.resolve({ id: run.id }) },
|
|
62
|
+
)
|
|
63
|
+
const eventsPayload = await eventsResponse.json()
|
|
64
|
+
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
listCount: Array.isArray(listPayload) ? listPayload.length : -1,
|
|
67
|
+
firstRunId: Array.isArray(listPayload) && listPayload[0] ? listPayload[0].id : null,
|
|
68
|
+
firstRunSource: Array.isArray(listPayload) && listPayload[0] ? listPayload[0].source : null,
|
|
69
|
+
firstRunStatus: Array.isArray(listPayload) && listPayload[0] ? listPayload[0].status : null,
|
|
70
|
+
detailId: detailPayload?.id || null,
|
|
71
|
+
detailSource: detailPayload?.source || null,
|
|
72
|
+
eventsCount: Array.isArray(eventsPayload) ? eventsPayload.length : -1,
|
|
73
|
+
eventSummary: Array.isArray(eventsPayload) && eventsPayload[0] ? eventsPayload[0].summary || null : null,
|
|
74
|
+
}))
|
|
75
|
+
`, { prefix: 'swarmclaw-runs-route-' })
|
|
76
|
+
|
|
77
|
+
assert.equal(output.listCount, 1)
|
|
78
|
+
assert.equal(output.firstRunId, output.detailId)
|
|
79
|
+
assert.equal(output.firstRunSource, 'structured schedule')
|
|
80
|
+
assert.equal(output.firstRunStatus, 'queued')
|
|
81
|
+
assert.equal(output.detailSource, 'structured schedule')
|
|
82
|
+
assert.equal(output.eventsCount >= 1, true)
|
|
83
|
+
assert.equal(typeof output.eventSummary, 'string')
|
|
84
|
+
})
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { listRuns } from '@/lib/server/runtime/session-run-manager'
|
|
3
|
-
import
|
|
3
|
+
import { listProtocolRuns } from '@/lib/server/protocols/protocol-queries'
|
|
4
|
+
import { protocolRunToSessionRunRecord } from '@/lib/server/runs/unified-run-records'
|
|
5
|
+
import type { ProtocolRunStatus, SessionRunStatus } from '@/types'
|
|
4
6
|
|
|
5
7
|
export const dynamic = 'force-dynamic'
|
|
6
8
|
|
|
9
|
+
function protocolStatusesForRunStatus(status?: SessionRunStatus): ProtocolRunStatus[] {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 'queued':
|
|
12
|
+
return ['draft']
|
|
13
|
+
case 'running':
|
|
14
|
+
return ['running', 'waiting', 'paused']
|
|
15
|
+
case 'completed':
|
|
16
|
+
return ['completed']
|
|
17
|
+
case 'failed':
|
|
18
|
+
return ['failed']
|
|
19
|
+
case 'cancelled':
|
|
20
|
+
return ['cancelled', 'archived']
|
|
21
|
+
default:
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
export async function GET(req: Request) {
|
|
8
27
|
const { searchParams } = new URL(req.url)
|
|
9
28
|
const sessionId = searchParams.get('sessionId') || undefined
|
|
@@ -11,6 +30,26 @@ export async function GET(req: Request) {
|
|
|
11
30
|
const limitRaw = searchParams.get('limit')
|
|
12
31
|
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined
|
|
13
32
|
|
|
14
|
-
const
|
|
33
|
+
const sessionRuns = listRuns({ sessionId, status, limit })
|
|
34
|
+
const fetchLimit = limit || 200
|
|
35
|
+
const scopedProtocolRuns = status
|
|
36
|
+
? protocolStatusesForRunStatus(status).flatMap((protocolStatus) => listProtocolRuns({
|
|
37
|
+
includeSystemOwned: true,
|
|
38
|
+
sessionId,
|
|
39
|
+
status: protocolStatus,
|
|
40
|
+
limit: fetchLimit,
|
|
41
|
+
}))
|
|
42
|
+
: listProtocolRuns({
|
|
43
|
+
includeSystemOwned: true,
|
|
44
|
+
sessionId,
|
|
45
|
+
limit: fetchLimit,
|
|
46
|
+
})
|
|
47
|
+
const protocolRuns = Array.from(new Map(scopedProtocolRuns.map((run) => [run.id, run])).values())
|
|
48
|
+
.filter((run) => run.status !== 'archived')
|
|
49
|
+
.map(protocolRunToSessionRunRecord)
|
|
50
|
+
.filter((run) => !status || run.status === status)
|
|
51
|
+
const runs = [...sessionRuns, ...protocolRuns]
|
|
52
|
+
.sort((left, right) => (right.queuedAt || 0) - (left.queuedAt || 0))
|
|
53
|
+
.slice(0, fetchLimit)
|
|
15
54
|
return NextResponse.json(runs)
|
|
16
55
|
}
|
|
@@ -6,6 +6,7 @@ import type { Agent, MemoryEntry, Session } from '@/types'
|
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
8
8
|
import { api } from '@/lib/app/api-client'
|
|
9
|
+
import { sortSessionsNewestFirst } from '@/lib/chat/new-session'
|
|
9
10
|
import { AgentAvatar } from './agent-avatar'
|
|
10
11
|
import { AgentFilesEditor } from './agent-files-editor'
|
|
11
12
|
import { OpenClawSkillsPanel } from './openclaw-skills-panel'
|
|
@@ -907,7 +908,7 @@ function SessionsSection({ agent }: { agent: Agent }) {
|
|
|
907
908
|
const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
|
|
908
909
|
|
|
909
910
|
const agentSessions = useMemo(() => {
|
|
910
|
-
return Object.values(sessions).filter((s) => s.agentId === agent.id)
|
|
911
|
+
return sortSessionsNewestFirst(Object.values(sessions).filter((s) => s.agentId === agent.id))
|
|
911
912
|
}, [sessions, agent.id])
|
|
912
913
|
|
|
913
914
|
if (agentSessions.length === 0) return null
|
|
@@ -31,6 +31,7 @@ import { api } from '@/lib/app/api-client'
|
|
|
31
31
|
import { messagesDiffer } from '@/lib/chat/chat-streaming-state'
|
|
32
32
|
import { createAssistantRenderId } from '@/lib/chat/assistant-render-id'
|
|
33
33
|
import { getSessionLastMessage } from '@/lib/chat/session-summary'
|
|
34
|
+
import { buildNewAgentSessionPayload, summarizeFirstMessageAsTitle } from '@/lib/chat/new-session'
|
|
34
35
|
import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
35
36
|
|
|
36
37
|
const DIRECT_PROMPT_SUGGESTIONS = [
|
|
@@ -57,6 +58,8 @@ export function ChatArea() {
|
|
|
57
58
|
const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
|
|
58
59
|
const removeSessionFromStore = useAppStore((s) => s.removeSession)
|
|
59
60
|
const refreshSession = useAppStore((s) => s.refreshSession)
|
|
61
|
+
const updateSessionInStore = useAppStore((s) => s.updateSessionInStore)
|
|
62
|
+
const setActiveSessionIdOverride = useAppStore((s) => s.setActiveSessionIdOverride)
|
|
60
63
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
61
64
|
const messages = useChatStore((s) => s.messages)
|
|
62
65
|
const messageStartIndex = useChatStore((s) => s.messageStartIndex)
|
|
@@ -172,6 +175,7 @@ export function ChatArea() {
|
|
|
172
175
|
const hasMultipleSources = connectorSources.size > 1 || (connectorSources.size > 0 && hasDirectMessages)
|
|
173
176
|
const [isDragging, setIsDragging] = useState(false)
|
|
174
177
|
const dragCounter = useRef(0)
|
|
178
|
+
const freshSessionIdRef = useRef<string | null>(null)
|
|
175
179
|
const setPendingImage = useChatStore((s) => s.setPendingImage)
|
|
176
180
|
|
|
177
181
|
useEffect(() => {
|
|
@@ -180,6 +184,13 @@ export function ChatArea() {
|
|
|
180
184
|
const requestedSessionId = sessionId
|
|
181
185
|
const chatState = useChatStore.getState()
|
|
182
186
|
const preserveLocalStream = chatState.streaming && chatState.streamingSessionId === requestedSessionId
|
|
187
|
+
if (freshSessionIdRef.current === requestedSessionId) {
|
|
188
|
+
freshSessionIdRef.current = null
|
|
189
|
+
setMessages([], { startIndex: 0, totalMessages: 0 })
|
|
190
|
+
useChatStore.setState({ hasMoreMessages: false })
|
|
191
|
+
setMessagesLoading(false)
|
|
192
|
+
return () => { cancelled = true }
|
|
193
|
+
}
|
|
183
194
|
// Clear stale messages immediately so the skeleton loader shows instead of
|
|
184
195
|
// the previous chat's messages flashing briefly during the fetch.
|
|
185
196
|
if (!preserveLocalStream) setMessages([], { startIndex: 0, totalMessages: 0 })
|
|
@@ -431,7 +442,7 @@ export function ChatArea() {
|
|
|
431
442
|
setDevServer(null)
|
|
432
443
|
}, [sessionId, setDevServer])
|
|
433
444
|
|
|
434
|
-
const handleClear = useCallback(async () => {
|
|
445
|
+
const handleClear = useCallback(async (mode: 'clear' | 'new-session' = 'clear') => {
|
|
435
446
|
setConfirmClear(false)
|
|
436
447
|
if (!sessionId) return
|
|
437
448
|
const targetSessionId = sessionId
|
|
@@ -448,7 +459,11 @@ export function ChatArea() {
|
|
|
448
459
|
await refreshSession(targetSessionId)
|
|
449
460
|
const { undoToken, cleared } = result
|
|
450
461
|
if (!undoToken) return
|
|
451
|
-
const clearedLabel =
|
|
462
|
+
const clearedLabel = mode === 'new-session'
|
|
463
|
+
? 'Started a fresh chat session.'
|
|
464
|
+
: cleared === 1
|
|
465
|
+
? '1 message cleared'
|
|
466
|
+
: `${cleared.toLocaleString()} messages cleared`
|
|
452
467
|
toast(clearedLabel, {
|
|
453
468
|
duration: 10_000,
|
|
454
469
|
action: {
|
|
@@ -487,6 +502,34 @@ export function ChatArea() {
|
|
|
487
502
|
setConfirmClear(true)
|
|
488
503
|
}, [])
|
|
489
504
|
|
|
505
|
+
const handleStartNewSession = useCallback(async () => {
|
|
506
|
+
if (!session) return
|
|
507
|
+
try {
|
|
508
|
+
const nextSession = await api<typeof session>('POST', '/chats', {
|
|
509
|
+
...buildNewAgentSessionPayload(session),
|
|
510
|
+
name: currentAgent?.name || session.name,
|
|
511
|
+
})
|
|
512
|
+
freshSessionIdRef.current = nextSession.id
|
|
513
|
+
updateSessionInStore(nextSession)
|
|
514
|
+
setActiveSessionIdOverride(nextSession.id)
|
|
515
|
+
toast.success('Started a new chat session.')
|
|
516
|
+
} catch (err) {
|
|
517
|
+
toast.error(`Could not start a new chat session: ${errorMessage(err)}`)
|
|
518
|
+
}
|
|
519
|
+
}, [currentAgent?.name, session, setActiveSessionIdOverride, updateSessionInStore])
|
|
520
|
+
|
|
521
|
+
const handleSend = useCallback(async (text: string) => {
|
|
522
|
+
if (!sessionId) return
|
|
523
|
+
if (session && messages.length === 0) {
|
|
524
|
+
const nextTitle = summarizeFirstMessageAsTitle(text, currentAgent?.name || session.name)
|
|
525
|
+
if (nextTitle && nextTitle !== session.name) {
|
|
526
|
+
updateSessionInStore({ ...session, name: nextTitle })
|
|
527
|
+
void api('PUT', `/chats/${sessionId}`, { name: nextTitle }).catch(() => {})
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
await sendMessage(text, { sessionId })
|
|
531
|
+
}, [currentAgent?.name, messages.length, sendMessage, session, sessionId, updateSessionInStore])
|
|
532
|
+
|
|
490
533
|
const handleDelete = useCallback(async () => {
|
|
491
534
|
setConfirmDelete(false)
|
|
492
535
|
if (!sessionId) return
|
|
@@ -496,8 +539,8 @@ export function ChatArea() {
|
|
|
496
539
|
}, [removeSessionFromStore, sessionId, setCurrentAgent])
|
|
497
540
|
|
|
498
541
|
const handlePrompt = useCallback((text: string) => {
|
|
499
|
-
|
|
500
|
-
}, [
|
|
542
|
+
void handleSend(text)
|
|
543
|
+
}, [handleSend])
|
|
501
544
|
|
|
502
545
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
503
546
|
e.preventDefault()
|
|
@@ -568,6 +611,7 @@ export function ChatArea() {
|
|
|
568
611
|
messageCount={messages.length}
|
|
569
612
|
onCompactComplete={handleCompactComplete}
|
|
570
613
|
onClearRequest={handleClearRequest}
|
|
614
|
+
onStartNewSession={handleStartNewSession}
|
|
571
615
|
/>
|
|
572
616
|
)}
|
|
573
617
|
{!isDesktop && (
|
|
@@ -589,6 +633,7 @@ export function ChatArea() {
|
|
|
589
633
|
messageCount={messages.length}
|
|
590
634
|
onCompactComplete={handleCompactComplete}
|
|
591
635
|
onClearRequest={handleClearRequest}
|
|
636
|
+
onStartNewSession={handleStartNewSession}
|
|
592
637
|
/>
|
|
593
638
|
)}
|
|
594
639
|
<DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
|
|
@@ -691,13 +736,13 @@ export function ChatArea() {
|
|
|
691
736
|
onClose={() => setDebugOpen(false)}
|
|
692
737
|
/>
|
|
693
738
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
739
|
+
<ChatInput
|
|
740
|
+
streaming={streamingForThisSession}
|
|
741
|
+
busy={streamingForThisSession || session.active === true}
|
|
742
|
+
onSend={handleSend}
|
|
743
|
+
onStop={stopStreaming}
|
|
744
|
+
extensionChatActions={extensionChatActions}
|
|
745
|
+
/>
|
|
701
746
|
|
|
702
747
|
<Dropdown open={menuOpen} onClose={() => setMenuOpen(false)}>
|
|
703
748
|
<DropdownItem onClick={() => {
|
|
@@ -720,7 +765,7 @@ export function ChatArea() {
|
|
|
720
765
|
message="Clear every message in this chat. Long-term memory, skills, and facts are preserved. You'll have 10 seconds to undo."
|
|
721
766
|
confirmLabel="Clear"
|
|
722
767
|
danger
|
|
723
|
-
onConfirm={handleClear}
|
|
768
|
+
onConfirm={() => { void handleClear('clear') }}
|
|
724
769
|
onCancel={() => setConfirmClear(false)}
|
|
725
770
|
/>
|
|
726
771
|
<ConfirmDialog
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useMemo, useRef, type ReactNode } from 'react'
|
|
4
|
+
import { Plus } from 'lucide-react'
|
|
4
5
|
import type { Session } from '@/types'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -17,6 +18,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
|
|
|
17
18
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
18
19
|
import { useNavigate } from '@/lib/app/navigation'
|
|
19
20
|
import { getEnabledToolIds } from '@/lib/capability-selection'
|
|
21
|
+
import { getNewSessionButtonTitle, hasResettableSessionRuntime } from '@/lib/chat/new-session'
|
|
20
22
|
import { ContextMeterBadge } from './context-meter-badge'
|
|
21
23
|
|
|
22
24
|
function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
|
|
@@ -84,9 +86,10 @@ interface Props {
|
|
|
84
86
|
messageCount?: number
|
|
85
87
|
onCompactComplete?: () => void
|
|
86
88
|
onClearRequest?: () => void
|
|
89
|
+
onStartNewSession?: () => void
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, connectorSources, connectorFilter, onConnectorFilterChange, hasMultipleSources, messageCount = 0, onCompactComplete, onClearRequest }: Props) {
|
|
92
|
+
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, connectorSources, connectorFilter, onConnectorFilterChange, hasMultipleSources, messageCount = 0, onCompactComplete, onClearRequest, onStartNewSession }: Props) {
|
|
90
93
|
const now = useNow()
|
|
91
94
|
const agentStatus = useChatStore((s) => s.agentStatus)
|
|
92
95
|
const agents = useAppStore((s) => s.agents)
|
|
@@ -114,6 +117,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
114
117
|
const renameInputRef = useRef<HTMLInputElement>(null)
|
|
115
118
|
const renameContainerRef = useRef<HTMLSpanElement>(null)
|
|
116
119
|
const liveStatus = agentStatus || null
|
|
120
|
+
const canStartNewSession = !streaming && !!onStartNewSession && (messageCount > 0 || hasResettableSessionRuntime(session))
|
|
121
|
+
const newSessionTitle = getNewSessionButtonTitle(session)
|
|
117
122
|
const connectorPresenceMeta = useMemo(() => {
|
|
118
123
|
if (!connector) return null
|
|
119
124
|
const lastAt = connectorPresence?.lastMessageAt
|
|
@@ -431,6 +436,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
431
436
|
onClearRequest={onClearRequest}
|
|
432
437
|
/>
|
|
433
438
|
)}
|
|
439
|
+
{canStartNewSession && (
|
|
440
|
+
<Tip label={newSessionTitle}>
|
|
441
|
+
<button
|
|
442
|
+
type="button"
|
|
443
|
+
onClick={onStartNewSession}
|
|
444
|
+
className="inline-flex items-center gap-1.5 rounded-[9px] border border-white/[0.06] bg-white/[0.03] px-2.5 py-1 text-[10px] font-600 text-text-3/70 transition-colors shrink-0 cursor-pointer hover:border-white/[0.15] hover:bg-white/[0.05] hover:text-text-2"
|
|
445
|
+
aria-label="Start a new chat session"
|
|
446
|
+
title={newSessionTitle}
|
|
447
|
+
>
|
|
448
|
+
<Plus className="h-3 w-3" aria-hidden="true" strokeWidth={2.2} />
|
|
449
|
+
<span>New chat</span>
|
|
450
|
+
</button>
|
|
451
|
+
</Tip>
|
|
452
|
+
)}
|
|
434
453
|
</div>
|
|
435
454
|
{liveStatus?.status && (
|
|
436
455
|
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
|