Skip to content

Commit c7e1c50

Browse files
unraidclaude
andcommitted
feat: 添加服务层增强与零散改进
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2247026 commit c7e1c50

23 files changed

Lines changed: 861 additions & 100 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2+
import { readFile, rm } from 'node:fs/promises'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import {
6+
resetStateForTests,
7+
setCwdState,
8+
setOriginalCwd,
9+
} from '../../bootstrap/state'
10+
import { getTaskListId } from '../../utils/tasks'
11+
import { getTeamFilePath } from '../../utils/swarm/teamHelpers'
12+
import { initializeAssistantTeam } from '../index'
13+
14+
let tempDir = ''
15+
let previousConfigDir: string | undefined
16+
17+
beforeEach(() => {
18+
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
19+
tempDir = join(
20+
tmpdir(),
21+
`assistant-team-${Date.now()}-${Math.random().toString(16).slice(2)}`,
22+
)
23+
process.env.CLAUDE_CONFIG_DIR = join(tempDir, 'config')
24+
resetStateForTests()
25+
setOriginalCwd(tempDir)
26+
setCwdState(tempDir)
27+
})
28+
29+
afterEach(async () => {
30+
resetStateForTests()
31+
if (previousConfigDir === undefined) {
32+
delete process.env.CLAUDE_CONFIG_DIR
33+
} else {
34+
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
35+
}
36+
await rm(tempDir, { recursive: true, force: true })
37+
})
38+
39+
describe('initializeAssistantTeam', () => {
40+
test('creates a session-scoped in-process team context and task list', async () => {
41+
const context = await initializeAssistantTeam()
42+
expect(context).toBeDefined()
43+
const teamContext = context!
44+
45+
expect(teamContext.teamName).toStartWith('assistant-')
46+
expect(teamContext.isLeader).toBe(true)
47+
expect(teamContext.selfAgentName).toBe('team-lead')
48+
expect(
49+
teamContext.teammates[teamContext.leadAgentId]?.tmuxSessionName,
50+
).toBe('in-process')
51+
expect(getTaskListId()).toBe(teamContext.teamName)
52+
53+
const raw = await readFile(getTeamFilePath(teamContext.teamName), 'utf-8')
54+
const teamFile = JSON.parse(raw)
55+
expect(teamFile.leadAgentId).toBe(teamContext.leadAgentId)
56+
expect(teamFile.members[0].backendType).toBe('in-process')
57+
expect(teamFile.members[0].agentType).toBe('assistant')
58+
})
59+
})

src/assistant/index.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
import { readFileSync } from 'fs'
22
import { join } from 'path'
3-
import { getKairosActive } from '../bootstrap/state.js'
3+
import { getKairosActive, getSessionId } from '../bootstrap/state.js'
4+
import type { AppState } from '../state/AppState.js'
5+
import { formatAgentId } from '../utils/agentId.js'
6+
import { getCwd } from '../utils/cwd.js'
47
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
8+
import { TEAM_LEAD_NAME } from '../utils/swarm/constants.js'
9+
import {
10+
getTeamFilePath,
11+
registerTeamForSessionCleanup,
12+
sanitizeName,
13+
writeTeamFileAsync,
14+
type TeamFile,
15+
} from '../utils/swarm/teamHelpers.js'
16+
import { assignTeammateColor } from '../utils/swarm/teammateLayoutManager.js'
17+
import {
18+
ensureTasksDir,
19+
resetTaskList,
20+
setLeaderTeamName,
21+
} from '../utils/tasks.js'
522

623
let _assistantForced = false
724

@@ -29,13 +46,67 @@ export function isAssistantForced(): boolean {
2946
* Pre-create an in-process team so Agent(name) can spawn teammates
3047
* without TeamCreate.
3148
*
32-
* Phase 1: returns undefined so main.tsx's `assistantTeamContext ?? computeInitialTeamContext()`
33-
* correctly falls back. Returning {} would bypass the ?? operator since {} is truthy.
34-
*
35-
* Phase 2: should return a full team context object matching AppState.teamContext shape.
49+
* Creates a session-scoped assistant team file and returns a full team
50+
* context object matching AppState.teamContext.
3651
*/
37-
export async function initializeAssistantTeam(): Promise<undefined> {
38-
return undefined
52+
export async function initializeAssistantTeam(): Promise<
53+
AppState['teamContext']
54+
> {
55+
const sessionId = getSessionId()
56+
const teamName = sanitizeName(`assistant-${sessionId.slice(0, 8)}`)
57+
const leadAgentId = formatAgentId(TEAM_LEAD_NAME, teamName)
58+
const teamFilePath = getTeamFilePath(teamName)
59+
const now = Date.now()
60+
const cwd = getCwd()
61+
const color = assignTeammateColor(leadAgentId)
62+
63+
const teamFile: TeamFile = {
64+
name: teamName,
65+
description: 'Assistant mode in-process team',
66+
createdAt: now,
67+
leadAgentId,
68+
leadSessionId: sessionId,
69+
members: [
70+
{
71+
agentId: leadAgentId,
72+
name: TEAM_LEAD_NAME,
73+
agentType: 'assistant',
74+
color,
75+
joinedAt: now,
76+
tmuxPaneId: '',
77+
cwd,
78+
subscriptions: [],
79+
backendType: 'in-process',
80+
},
81+
],
82+
}
83+
84+
await writeTeamFileAsync(teamName, teamFile)
85+
registerTeamForSessionCleanup(teamName)
86+
await resetTaskList(teamName)
87+
await ensureTasksDir(teamName)
88+
setLeaderTeamName(teamName)
89+
90+
return {
91+
teamName,
92+
teamFilePath,
93+
leadAgentId,
94+
selfAgentId: leadAgentId,
95+
selfAgentName: TEAM_LEAD_NAME,
96+
isLeader: true,
97+
selfAgentColor: color,
98+
teammates: {
99+
[leadAgentId]: {
100+
name: TEAM_LEAD_NAME,
101+
agentType: 'assistant',
102+
color,
103+
tmuxSessionName: 'in-process',
104+
tmuxPaneId: 'leader',
105+
cwd,
106+
spawnedAt: now,
107+
},
108+
},
109+
}
39110
}
40111

41112
/**

src/bridge/bridgeMain.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,7 +1963,6 @@ NOTES
19631963
- You must be logged in with a Claude account that has a subscription
19641964
- Run \`claude\` first in the directory to accept the workspace trust dialog
19651965
${serverNote}`
1966-
// biome-ignore lint/suspicious/noConsole: intentional help output
19671966
console.log(help)
19681967
}
19691968

@@ -2002,7 +2001,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
20022001
return
20032002
}
20042003
if (parsed.error) {
2005-
// biome-ignore lint/suspicious/noConsole: intentional error output
20062004
console.error(`Error: ${parsed.error}`)
20072005
// eslint-disable-next-line custom-rules/no-process-exit
20082006
process.exit(1)
@@ -2041,7 +2039,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
20412039
const { PERMISSION_MODES } = await import('../types/permissions.js')
20422040
const valid: readonly string[] = PERMISSION_MODES
20432041
if (!valid.includes(permissionMode)) {
2044-
// biome-ignore lint/suspicious/noConsole: intentional error output
20452042
console.error(
20462043
`Error: Invalid permission mode '${permissionMode}'. Valid modes: ${valid.join(', ')}`,
20472044
)
@@ -2084,7 +2081,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
20842081
Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
20852082
sleep(500, undefined, { unref: true }),
20862083
]).catch(() => {})
2087-
// biome-ignore lint/suspicious/noConsole: intentional error output
20882084
console.error(
20892085
'Error: Multi-session Remote Control is not enabled for your account yet.',
20902086
)
@@ -2101,7 +2097,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
21012097
// The bridge bypasses main.tsx (which renders the interactive TrustDialog via showSetupScreens),
21022098
// so we must verify trust was previously established by a normal `claude` session.
21032099
if (!checkHasTrustDialogAccepted()) {
2104-
// biome-ignore lint/suspicious/noConsole:: intentional console output
21052100
console.error(
21062101
`Error: Workspace not trusted. Please run \`claude\` in ${dir} first to review and accept the workspace trust dialog.`,
21072102
)
@@ -2118,7 +2113,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
21182113

21192114
const bridgeToken = getBridgeAccessToken()
21202115
if (!bridgeToken) {
2121-
// biome-ignore lint/suspicious/noConsole:: intentional console output
21222116
console.error(BRIDGE_LOGIN_ERROR)
21232117
// eslint-disable-next-line custom-rules/no-process-exit
21242118
process.exit(1)
@@ -2137,7 +2131,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
21372131
input: process.stdin,
21382132
output: process.stdout,
21392133
})
2140-
// biome-ignore lint/suspicious/noConsole:: intentional console output
21412134
console.log(
21422135
'\nRemote Control lets you access this CLI session from the web (claude.ai/code)\nor the Claude app, so you can pick up where you left off on any device.\n\nYou can disconnect remote access anytime by running /remote-control again.\n',
21432136
)
@@ -2169,7 +2162,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
21692162
)
21702163
const found = await readBridgePointerAcrossWorktrees(dir)
21712164
if (!found) {
2172-
// biome-ignore lint/suspicious/noConsole: intentional error output
21732165
console.error(
21742166
`Error: No recent session found in this directory or its worktrees. Run \`claude remote-control\` to start a new one.`,
21752167
)
@@ -2180,7 +2172,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
21802172
const ageMin = Math.round(pointer.ageMs / 60_000)
21812173
const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`
21822174
const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : ''
2183-
// biome-ignore lint/suspicious/noConsole: intentional info output
21842175
console.error(
21852176
`Resuming session ${pointer.sessionId} (${ageStr} ago)${fromWt}\u2026`,
21862177
)
@@ -2201,7 +2192,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
22012192
!baseUrl.includes('localhost') &&
22022193
!baseUrl.includes('127.0.0.1')
22032194
) {
2204-
// biome-ignore lint/suspicious/noConsole:: intentional console output
22052195
console.error(
22062196
'Error: Remote Control base URL uses HTTP. Only HTTPS or localhost HTTP is allowed.',
22072197
)
@@ -2237,7 +2227,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
22372227
? getCurrentProjectConfig().remoteControlSpawnMode
22382228
: undefined
22392229
if (savedSpawnMode === 'worktree' && !worktreeAvailable) {
2240-
// biome-ignore lint/suspicious/noConsole: intentional warning output
22412230
console.error(
22422231
'Warning: Saved spawn mode is worktree but this directory is not a git repository. Falling back to same-dir.',
22432232
)
@@ -2264,7 +2253,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
22642253
input: process.stdin,
22652254
output: process.stdout,
22662255
})
2267-
// biome-ignore lint/suspicious/noConsole: intentional dialog output
22682256
console.log(
22692257
`\nClaude Remote Control is launching in spawn mode which lets you create new sessions in this project from Claude Code on Web or your Mobile app. Learn more here: https://code.claude.com/docs/en/remote-control\n\n` +
22702258
`Spawn mode for this project:\n` +
@@ -2343,7 +2331,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
23432331
// Only reachable via explicit --spawn=worktree (default is same-dir);
23442332
// saved worktree pref was already guarded above.
23452333
if (spawnMode === 'worktree' && !worktreeAvailable) {
2346-
// biome-ignore lint/suspicious/noConsole: intentional error output
23472334
console.error(
23482335
`Error: Worktree mode requires a git repository or WorktreeCreate hooks configured. Use --spawn=session for single-session mode.`,
23492336
)
@@ -2378,7 +2365,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
23782365
try {
23792366
validateBridgeId(resumeSessionId, 'sessionId')
23802367
} catch {
2381-
// biome-ignore lint/suspicious/noConsole: intentional error output
23822368
console.error(
23832369
`Error: Invalid session ID "${resumeSessionId}". Session IDs must not contain unsafe characters.`,
23842370
)
@@ -2404,7 +2390,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
24042390
const { clearBridgePointer } = await import('./bridgePointer.js')
24052391
await clearBridgePointer(resumePointerDir)
24062392
}
2407-
// biome-ignore lint/suspicious/noConsole: intentional error output
24082393
console.error(
24092394
`Error: Session ${resumeSessionId} not found. It may have been archived or expired, or your login may have lapsed (run \`claude /login\`).`,
24102395
)
@@ -2416,7 +2401,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
24162401
const { clearBridgePointer } = await import('./bridgePointer.js')
24172402
await clearBridgePointer(resumePointerDir)
24182403
}
2419-
// biome-ignore lint/suspicious/noConsole: intentional error output
24202404
console.error(
24212405
`Error: Session ${resumeSessionId} has no environment_id. It may never have been attached to a bridge.`,
24222406
)
@@ -2470,7 +2454,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
24702454
status: err instanceof BridgeFatalError ? err.status : undefined,
24712455
})
24722456
// Registration failures are fatal — print a clean message instead of a stack trace.
2473-
// biome-ignore lint/suspicious/noConsole:: intentional console output
24742457
console.error(
24752458
err instanceof BridgeFatalError && err.status === 404
24762459
? 'Remote Control environments are not available for your account.'
@@ -2495,7 +2478,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
24952478
`Bridge resume env mismatch: requested ${reuseEnvironmentId}, backend returned ${environmentId}. Falling back to fresh session.`,
24962479
),
24972480
)
2498-
// biome-ignore lint/suspicious/noConsole: intentional warning output
24992481
console.warn(
25002482
`Warning: Could not resume session ${resumeSessionId} — its environment has expired. Creating a fresh session instead.`,
25012483
)
@@ -2546,7 +2528,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
25462528
const { clearBridgePointer } = await import('./bridgePointer.js')
25472529
await clearBridgePointer(resumePointerDir)
25482530
}
2549-
// biome-ignore lint/suspicious/noConsole: intentional error output
25502531
console.error(
25512532
isFatal
25522533
? `Error: ${errorMessage(err)}`

src/cli/exit.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
/** Write an error message to stderr (if given) and exit with code 1. */
1919
export function cliError(msg?: string): never {
20-
// biome-ignore lint/suspicious/noConsole: centralized CLI error output
2120
if (msg) console.error(msg)
2221
process.exit(1)
2322
return undefined as never

src/cli/handlers/agents.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,9 @@ export async function agentsHandler(): Promise<void> {
5959
}
6060

6161
if (lines.length === 0) {
62-
// biome-ignore lint/suspicious/noConsole:: intentional console output
6362
console.log('No agents found.')
6463
} else {
65-
// biome-ignore lint/suspicious/noConsole:: intentional console output
6664
console.log(`${totalActive} active agents\n`)
67-
// biome-ignore lint/suspicious/noConsole:: intentional console output
6865
console.log(lines.join('\n').trimEnd())
6966
}
7067
}

0 commit comments

Comments
 (0)