@swarmclawai/swarmclaw 1.7.0 → 1.7.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/README.md +15 -9
- package/bin/swarmclaw.js +87 -0
- package/package.json +1 -1
- package/src/app/home/page.tsx +19 -10
- package/src/cli/index.js +8 -2
- package/src/cli/index.ts +12 -3
- package/src/components/agents/inspector-panel.tsx +25 -3
- package/src/components/auth/setup-wizard/index.tsx +6 -2
- package/src/components/auth/setup-wizard/step-next.tsx +46 -39
- package/src/components/auth/setup-wizard/step-providers.tsx +76 -142
- package/src/components/auth/setup-wizard/types.ts +5 -2
- package/src/components/auth/setup-wizard/utils.test.ts +0 -19
- package/src/components/auth/setup-wizard/utils.ts +0 -69
- package/src/components/chat/chat-card.tsx +5 -0
- package/src/components/home/home-launchpad.tsx +123 -71
- package/src/lib/home-launchpad.test.ts +1 -31
- package/src/lib/home-launchpad.ts +0 -58
- package/src/lib/providers/cli-utils.test.ts +65 -1
- package/src/lib/providers/cli-utils.ts +22 -1
- package/src/lib/providers/codex-cli.ts +71 -75
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
- package/src/stores/slices/session-slice.test.ts +40 -2
- package/src/stores/slices/session-slice.ts +41 -1
|
@@ -435,18 +435,25 @@ export async function finalizeChatTurn(params: {
|
|
|
435
435
|
;(current as unknown as Record<string, unknown>)[key] = normalized
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
|
+
const preferRunValue = (runValue: unknown, fallbackValue: unknown) => (
|
|
439
|
+
runValue !== undefined ? runValue : fallbackValue
|
|
440
|
+
)
|
|
438
441
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
persistField('
|
|
442
|
-
persistField('
|
|
443
|
-
persistField('
|
|
444
|
-
persistField('
|
|
445
|
-
persistField('
|
|
446
|
-
persistField('
|
|
447
|
-
persistField('
|
|
448
|
-
|
|
449
|
-
|
|
442
|
+
// Provider handlers receive `sessionForRun` and may mutate CLI resume IDs there.
|
|
443
|
+
// Persist from run-session first, allowing null to intentionally clear IDs.
|
|
444
|
+
persistField('claudeSessionId', preferRunValue(sessionForRun.claudeSessionId, session.claudeSessionId))
|
|
445
|
+
persistField('codexThreadId', preferRunValue(sessionForRun.codexThreadId, session.codexThreadId))
|
|
446
|
+
persistField('opencodeSessionId', preferRunValue(sessionForRun.opencodeSessionId, session.opencodeSessionId))
|
|
447
|
+
persistField('geminiSessionId', preferRunValue(sessionForRun.geminiSessionId, session.geminiSessionId))
|
|
448
|
+
persistField('copilotSessionId', preferRunValue(sessionForRun.copilotSessionId, session.copilotSessionId))
|
|
449
|
+
persistField('droidSessionId', preferRunValue(sessionForRun.droidSessionId, session.droidSessionId))
|
|
450
|
+
persistField('cursorSessionId', preferRunValue(sessionForRun.cursorSessionId, session.cursorSessionId))
|
|
451
|
+
persistField('qwenSessionId', preferRunValue(sessionForRun.qwenSessionId, session.qwenSessionId))
|
|
452
|
+
persistField('acpSessionId', preferRunValue(sessionForRun.acpSessionId, session.acpSessionId))
|
|
453
|
+
|
|
454
|
+
const sourceResume = (sessionForRun.delegateResumeIds && typeof sessionForRun.delegateResumeIds === 'object')
|
|
455
|
+
? sessionForRun.delegateResumeIds
|
|
456
|
+
: session.delegateResumeIds
|
|
450
457
|
if (sourceResume && typeof sourceResume === 'object') {
|
|
451
458
|
const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
|
|
452
459
|
? current.delegateResumeIds
|
|
@@ -454,14 +461,14 @@ export async function finalizeChatTurn(params: {
|
|
|
454
461
|
const sr = sourceResume as Record<string, unknown>
|
|
455
462
|
const cr = currentResume as Record<string, unknown>
|
|
456
463
|
const nextResume = {
|
|
457
|
-
claudeCode: normalizeResumeId(sr.claudeCode
|
|
458
|
-
codex: normalizeResumeId(sr.codex
|
|
459
|
-
opencode: normalizeResumeId(sr.opencode
|
|
460
|
-
gemini: normalizeResumeId(sr.gemini
|
|
461
|
-
copilot: normalizeResumeId(sr.copilot
|
|
462
|
-
droid: normalizeResumeId(sr.droid
|
|
463
|
-
cursor: normalizeResumeId(sr.cursor
|
|
464
|
-
qwen: normalizeResumeId(sr.qwen
|
|
464
|
+
claudeCode: normalizeResumeId(preferRunValue(sr.claudeCode, cr.claudeCode)),
|
|
465
|
+
codex: normalizeResumeId(preferRunValue(sr.codex, cr.codex)),
|
|
466
|
+
opencode: normalizeResumeId(preferRunValue(sr.opencode, cr.opencode)),
|
|
467
|
+
gemini: normalizeResumeId(preferRunValue(sr.gemini, cr.gemini)),
|
|
468
|
+
copilot: normalizeResumeId(preferRunValue(sr.copilot, cr.copilot)),
|
|
469
|
+
droid: normalizeResumeId(preferRunValue(sr.droid, cr.droid)),
|
|
470
|
+
cursor: normalizeResumeId(preferRunValue(sr.cursor, cr.cursor)),
|
|
471
|
+
qwen: normalizeResumeId(preferRunValue(sr.qwen, cr.qwen)),
|
|
465
472
|
}
|
|
466
473
|
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
467
474
|
current.delegateResumeIds = nextResume
|
|
@@ -32,11 +32,49 @@ test('selectActiveSessionId prefers override when present', () => {
|
|
|
32
32
|
assert.equal(selectActiveSessionId(state), 'task-1')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
test('selectActiveSessionId
|
|
35
|
+
test('selectActiveSessionId chooses most recently active session for current agent', () => {
|
|
36
36
|
const state = makeState({
|
|
37
37
|
currentAgentId: 'agent-1',
|
|
38
38
|
agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
|
|
39
|
-
sessions: {
|
|
39
|
+
sessions: {
|
|
40
|
+
'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 100 } as unknown as Session,
|
|
41
|
+
'old-1': { ...makeSession('old-1'), agentId: 'agent-1', lastActiveAt: 90 } as unknown as Session,
|
|
42
|
+
'latest-1': { ...makeSession('latest-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 1 } as unknown as Session,
|
|
43
|
+
'other-agent': { ...makeSession('other-agent'), agentId: 'agent-2', lastActiveAt: 999 } as unknown as Session,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
assert.equal(selectActiveSessionId(state), 'latest-1')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('selectActiveSessionId prefers most recent session with content over newer empty thread session', () => {
|
|
50
|
+
const state = makeState({
|
|
51
|
+
currentAgentId: 'agent-1',
|
|
52
|
+
agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
|
|
53
|
+
sessions: {
|
|
54
|
+
'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 300 } as unknown as Session,
|
|
55
|
+
'work-1': { ...makeSession('work-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 2 } as unknown as Session,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
assert.equal(selectActiveSessionId(state), 'work-1')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('selectActiveSessionId falls back to thread session when agent has no loaded sessions', () => {
|
|
62
|
+
const state = makeState({
|
|
63
|
+
currentAgentId: 'agent-1',
|
|
64
|
+
agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
|
|
65
|
+
sessions: { 'unrelated': { ...makeSession('unrelated'), agentId: 'agent-2' } as unknown as Session },
|
|
66
|
+
})
|
|
67
|
+
assert.equal(selectActiveSessionId(state), 'thread-1')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('selectActiveSessionId falls back to thread session when all loaded sessions are empty', () => {
|
|
71
|
+
const state = makeState({
|
|
72
|
+
currentAgentId: 'agent-1',
|
|
73
|
+
agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
|
|
74
|
+
sessions: {
|
|
75
|
+
'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 120 } as unknown as Session,
|
|
76
|
+
'empty-newer': { ...makeSession('empty-newer'), agentId: 'agent-1', lastActiveAt: 220 } as unknown as Session,
|
|
77
|
+
},
|
|
40
78
|
})
|
|
41
79
|
assert.equal(selectActiveSessionId(state), 'thread-1')
|
|
42
80
|
})
|
|
@@ -8,6 +8,46 @@ import { createLoader, createInflightDeduplicator } from '../store-utils'
|
|
|
8
8
|
|
|
9
9
|
const sessionRefreshDedup = createInflightDeduplicator('sessionSlice_inflightRefreshes')
|
|
10
10
|
|
|
11
|
+
function getSessionSortScore(session: Session): number {
|
|
12
|
+
return session.lastAssistantAt
|
|
13
|
+
|| session.lastActiveAt
|
|
14
|
+
|| session.updatedAt
|
|
15
|
+
|| session.createdAt
|
|
16
|
+
|| 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasSessionContent(session: Session): boolean {
|
|
20
|
+
if (typeof session.messageCount === 'number' && Number.isFinite(session.messageCount) && session.messageCount > 0) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
if (session.lastMessageSummary) return true
|
|
24
|
+
return Array.isArray(session.messages) && session.messages.length > 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getLatestAgentSessionId(s: AppState, agentId: string, threadSessionId?: string | null): string | null {
|
|
28
|
+
let bestAnyId: string | null = null
|
|
29
|
+
let bestAnyScore = Number.NEGATIVE_INFINITY
|
|
30
|
+
let bestWithContentId: string | null = null
|
|
31
|
+
let bestWithContentScore = Number.NEGATIVE_INFINITY
|
|
32
|
+
|
|
33
|
+
for (const [sessionId, session] of Object.entries(s.sessions)) {
|
|
34
|
+
if (session.agentId !== agentId) continue
|
|
35
|
+
const score = getSessionSortScore(session)
|
|
36
|
+
if (score > bestAnyScore) {
|
|
37
|
+
bestAnyScore = score
|
|
38
|
+
bestAnyId = sessionId
|
|
39
|
+
}
|
|
40
|
+
if (hasSessionContent(session) && score > bestWithContentScore) {
|
|
41
|
+
bestWithContentScore = score
|
|
42
|
+
bestWithContentId = sessionId
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (bestWithContentId) return bestWithContentId
|
|
47
|
+
if (threadSessionId && s.sessions[threadSessionId]?.agentId === agentId) return threadSessionId
|
|
48
|
+
return bestAnyId
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
/** Derive the active session ID from the current agent — no stored `currentSessionId`. */
|
|
12
52
|
export function selectActiveSessionId(s: AppState): string | null {
|
|
13
53
|
if (s.activeSessionIdOverride && s.sessions[s.activeSessionIdOverride]) {
|
|
@@ -15,7 +55,7 @@ export function selectActiveSessionId(s: AppState): string | null {
|
|
|
15
55
|
}
|
|
16
56
|
if (!s.currentAgentId) return null
|
|
17
57
|
const agent = s.agents[s.currentAgentId]
|
|
18
|
-
return agent?.threadSessionId
|
|
58
|
+
return getLatestAgentSessionId(s, s.currentAgentId, agent?.threadSessionId) || agent?.threadSessionId || null
|
|
19
59
|
}
|
|
20
60
|
|
|
21
61
|
export interface SessionSlice {
|