@agent-spaces/server 0.2.8 → 0.2.9

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.
@@ -218,7 +218,7 @@ function buildChainItems(lines, finalTextRange, finalText, workspaceRoot, toolDe
218
218
  }
219
219
  }
220
220
  flushMessageBuffer();
221
- return items.slice(0, 40);
221
+ return items;
222
222
  }
223
223
  function buildToolTodo(line, index, workspaceRoot, toolDetails, toolDetailMatchCounts) {
224
224
  const summary = summarizeToolLine(line, workspaceRoot);
package/dist/app.js CHANGED
@@ -27,6 +27,7 @@ import subscriptionRouter from './routes/subscription.js';
27
27
  import agentSseRouter from './routes/agent-sse.js';
28
28
  import searchRouter from './routes/search.js';
29
29
  import notificationRouter from './routes/notification.js';
30
+ import { getUserSettings, setUserAvatarUrl, removeUserAvatarUrl } from './storage/user-settings-store.js';
30
31
  import { authMiddleware, verifyToken } from './middleware/auth.js';
31
32
  import { handleConnection } from './ws/handler.js';
32
33
  import { startScheduler, stopScheduler } from './agents/scheduler-agent.js';
@@ -50,7 +51,7 @@ app.use('/api', authMiddleware);
50
51
  const publicDir = resolveRuntimeDir('public');
51
52
  app.use('/public', express.static(publicDir));
52
53
  app.get('/api/health', (_req, res) => {
53
- res.json({ status: 'ok', timestamp: new Date().toISOString() });
54
+ res.json({ status: 'ok', timestamp: new Date().toISOString(), platform: process.platform });
54
55
  });
55
56
  app.use('/api/auth', authRouter);
56
57
  // Avatar upload
@@ -75,6 +76,21 @@ app.post('/api/upload/avatar', async (req, res) => {
75
76
  await writeFile(join(avatarsDir, name), Buffer.from(base64, 'base64'));
76
77
  res.json({ url: `/static/avatars/${name}` });
77
78
  });
79
+ // User settings
80
+ app.get('/api/user/settings', (_req, res) => {
81
+ const settings = getUserSettings();
82
+ res.json({ avatarUrl: settings.avatarUrl ?? null });
83
+ });
84
+ app.put('/api/user/settings', (req, res) => {
85
+ const { avatarUrl } = req.body;
86
+ if (avatarUrl === null || avatarUrl === undefined) {
87
+ removeUserAvatarUrl();
88
+ }
89
+ else if (typeof avatarUrl === 'string') {
90
+ setUserAvatarUrl(avatarUrl);
91
+ }
92
+ res.json({ ok: true });
93
+ });
78
94
  app.use('/api/workspaces', workspaceRouter);
79
95
  app.use('/api/workspaces/:id/files', fileRouter);
80
96
  app.use('/api/workspaces/:id/channels', channelRouter);
@@ -3,6 +3,7 @@ import { exec } from 'child_process';
3
3
  import { resolve, join } from 'path';
4
4
  import * as fileService from '../services/file.js';
5
5
  import { getDataDir } from '../storage/json-store.js';
6
+ const EDITOR_STATE_PATH = '.agentspace/editor-state.json';
6
7
  const router = Router({ mergeParams: true });
7
8
  router.get('/tree', async (req, res) => {
8
9
  const ws = fileService.getWorkspace(req.params.id);
@@ -68,6 +69,43 @@ router.delete('/', async (req, res) => {
68
69
  }
69
70
  res.json({ ok: true });
70
71
  });
72
+ router.get('/editor-state', async (req, res) => {
73
+ const ws = fileService.getWorkspace(req.params.id);
74
+ if (!ws) {
75
+ res.status(404).json({ error: 'Workspace not found' });
76
+ return;
77
+ }
78
+ const result = await fileService.readFileContent(ws, EDITOR_STATE_PATH);
79
+ if (!result) {
80
+ res.json({ openFilePaths: [], activeFilePath: null });
81
+ return;
82
+ }
83
+ try {
84
+ const state = JSON.parse(result.content);
85
+ res.json(state);
86
+ }
87
+ catch {
88
+ res.json({ openFilePaths: [], activeFilePath: null });
89
+ }
90
+ });
91
+ router.put('/editor-state', async (req, res) => {
92
+ const ws = fileService.getWorkspace(req.params.id);
93
+ if (!ws) {
94
+ res.status(404).json({ error: 'Workspace not found' });
95
+ return;
96
+ }
97
+ const { openFilePaths, activeFilePath } = req.body;
98
+ if (!Array.isArray(openFilePaths)) {
99
+ res.status(400).json({ error: 'openFilePaths is required' });
100
+ return;
101
+ }
102
+ const ok = await fileService.writeFileContent(ws, EDITOR_STATE_PATH, JSON.stringify({ openFilePaths, activeFilePath }, null, 2));
103
+ if (!ok) {
104
+ res.status(500).json({ error: 'Failed to save editor state' });
105
+ return;
106
+ }
107
+ res.json({ ok: true });
108
+ });
71
109
  router.post('/reveal', async (req, res) => {
72
110
  const ws = fileService.getWorkspace(req.params.id);
73
111
  if (!ws) {
@@ -49,6 +49,31 @@ export function updateMessage(workspaceId, channelId, messageId, data) {
49
49
  writeJsonFile(path, messages);
50
50
  return updated;
51
51
  }
52
+ export function appendMessageReply(workspaceId, channelId, messageId, data) {
53
+ const path = messageFilePath(workspaceId, channelId);
54
+ const messages = readJsonFile(path) || [];
55
+ const index = messages.findIndex((message) => message.id === messageId);
56
+ if (index === -1)
57
+ return null;
58
+ const reply = {
59
+ id: uuid(),
60
+ senderId: data.senderId,
61
+ senderRole: data.senderRole,
62
+ content: data.content,
63
+ status: data.status,
64
+ attachments: data.attachments,
65
+ parts: data.parts,
66
+ metadata: data.metadata,
67
+ createdAt: new Date().toISOString(),
68
+ };
69
+ const updated = {
70
+ ...messages[index],
71
+ replies: [...(messages[index].replies ?? []), reply],
72
+ };
73
+ messages[index] = updated;
74
+ writeJsonFile(path, messages);
75
+ return updated;
76
+ }
52
77
  export function deleteMessage(workspaceId, channelId, messageId) {
53
78
  const path = messageFilePath(workspaceId, channelId);
54
79
  const messages = readJsonFile(path) || [];
@@ -1,5 +1,6 @@
1
1
  import pty from 'node-pty';
2
2
  import { v4 as uuid } from 'uuid';
3
+ const MAX_BUFFER_LINES = 100;
3
4
  const sessions = new Map();
4
5
  export function createSession(workspaceId, cwd, onOutput, onExit, shell, env) {
5
6
  const id = uuid();
@@ -18,9 +19,16 @@ export function createSession(workspaceId, cwd, onOutput, onExit, shell, env) {
18
19
  cwd,
19
20
  env: ptyEnv,
20
21
  });
21
- ptyProcess.onData((data) => onOutput(id, data));
22
+ const session = { id, pty: ptyProcess, workspaceId, cwd, buffer: [] };
23
+ ptyProcess.onData((data) => {
24
+ session.buffer.push(data);
25
+ if (session.buffer.length > MAX_BUFFER_LINES) {
26
+ session.buffer = session.buffer.slice(-MAX_BUFFER_LINES);
27
+ }
28
+ onOutput(id, data);
29
+ });
22
30
  ptyProcess.onExit(({ exitCode }) => onExit(id, exitCode ?? 0));
23
- sessions.set(id, { id, pty: ptyProcess, workspaceId, cwd });
31
+ sessions.set(id, session);
24
32
  return id;
25
33
  }
26
34
  export function write(sessionId, data) {
@@ -179,7 +179,7 @@ export function mapWorkflowToTaskDrafts(template) {
179
179
  }
180
180
  return template.nodes.map(node => ({
181
181
  key: node.id,
182
- title: node.data.taskTitleTemplate || `Execute ${node.data.label}`,
182
+ title: node.data.taskTitleTemplate || node.data.label,
183
183
  description: node.data.taskDescriptionTemplate || `Task assigned to ${node.data.label} (${node.data.role})`,
184
184
  agentConfigId: node.data.agentConfigId,
185
185
  dependsOnKeys: dependsOn.get(node.id)?.length ? dependsOn.get(node.id) : undefined,
@@ -0,0 +1,17 @@
1
+ import { join } from 'node:path';
2
+ import { readJsonFile, writeJsonFile, getDataDir } from './json-store.js';
3
+ const FILE = () => join(getDataDir(), 'user-settings.json');
4
+ export function getUserSettings() {
5
+ return readJsonFile(FILE()) ?? {};
6
+ }
7
+ export function setUserAvatarUrl(url) {
8
+ const settings = getUserSettings();
9
+ settings.avatarUrl = url;
10
+ writeJsonFile(FILE(), settings);
11
+ }
12
+ export function removeUserAvatarUrl() {
13
+ const settings = getUserSettings();
14
+ delete settings.avatarUrl;
15
+ writeJsonFile(FILE(), settings);
16
+ }
17
+ //# sourceMappingURL=user-settings-store.js.map
@@ -42,6 +42,12 @@ function formatConversationHistory(history) {
42
42
  if (!text.trim())
43
43
  continue;
44
44
  lines.push(`[${role}]: ${text}`);
45
+ for (const reply of msg.replies ?? []) {
46
+ const replyRole = reply.senderId === 'user' ? 'User' : (reply.senderRole || 'Agent');
47
+ const replyText = reply.senderId === 'user' ? stripHtml(reply.content) : getReplyFinalMessage(reply);
48
+ if (replyText.trim())
49
+ lines.push(`[${replyRole} reply]: ${replyText}`);
50
+ }
45
51
  }
46
52
  return lines;
47
53
  }
@@ -58,6 +64,19 @@ function getAgentFinalMessage(message) {
58
64
  return '';
59
65
  return stripToolLikeHistoryLines(stripHtml(message.content));
60
66
  }
67
+ function getReplyFinalMessage(reply) {
68
+ const finalTextParts = reply.parts
69
+ ?.filter((part) => part.type === 'text')
70
+ .map((part) => part.text.trim())
71
+ .filter(Boolean);
72
+ if (finalTextParts?.length)
73
+ return finalTextParts.join('\n\n');
74
+ if (reply.parts?.length)
75
+ return '';
76
+ if (reply.status && reply.status !== 'completed')
77
+ return '';
78
+ return stripToolLikeHistoryLines(stripHtml(reply.content));
79
+ }
61
80
  function stripToolLikeHistoryLines(content) {
62
81
  const kept = content.split(/\r?\n/).filter((line) => {
63
82
  const trimmed = line.trim();
@@ -239,13 +239,14 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
239
239
  askUserQuestions,
240
240
  success: true,
241
241
  });
242
- if (parts.length === 0)
242
+ const displayParts = buildContinuationParts(existingMessage?.parts, session.id, options.appendUserMessage, parts);
243
+ if (displayParts.length === 0)
243
244
  return;
244
245
  const status = askUserQuestions.some((question) => !question.answer) ? 'waiting_for_user' : 'streaming';
245
246
  const live = updateMessage(workspaceId, channelId, pending.id, {
246
247
  content: liveOutput.join('\n') || pending.content,
247
248
  status,
248
- parts,
249
+ parts: displayParts,
249
250
  metadata: {
250
251
  ...pending.metadata,
251
252
  duration: Date.now() - startTime,
@@ -354,6 +355,21 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
354
355
  const displayOutput = mergeRuntimeOutput(liveOutput, result.output);
355
356
  if (shouldWaitForUserAnswer(askUserQuestions, result.summary, result.error, displayOutput)) {
356
357
  const waitingOutput = stripAskUserQuestionErrorLines(liveOutput);
358
+ const waitingParts = buildContinuationParts(existingMessage?.parts, session.id, options.appendUserMessage, buildAgentMessageParts({
359
+ sessionId: session.id,
360
+ workspaceRoot: workspace?.boundDirs?.[0],
361
+ presetName: preset.name || preset.role,
362
+ role: preset.role,
363
+ model: preset.modelId,
364
+ systemPrompt: preset.systemPrompt,
365
+ mcpServers: Object.keys(mcpServers ?? {}),
366
+ skills,
367
+ output: waitingOutput,
368
+ reasoning: liveReasoning,
369
+ toolDetails,
370
+ askUserQuestions,
371
+ success: true,
372
+ }));
357
373
  const waiting = updateMessage(workspaceId, channelId, pending.id, {
358
374
  content: waitingOutput.join('\n') || pending.content,
359
375
  type: 'text',
@@ -365,21 +381,7 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
365
381
  summary: 'Waiting for user answer',
366
382
  duration: Date.now() - startTime,
367
383
  },
368
- parts: buildAgentMessageParts({
369
- sessionId: session.id,
370
- workspaceRoot: workspace?.boundDirs?.[0],
371
- presetName: preset.name || preset.role,
372
- role: preset.role,
373
- model: preset.modelId,
374
- systemPrompt: preset.systemPrompt,
375
- mcpServers: Object.keys(mcpServers ?? {}),
376
- skills,
377
- output: waitingOutput,
378
- reasoning: liveReasoning,
379
- toolDetails,
380
- askUserQuestions,
381
- success: true,
382
- }),
384
+ parts: waitingParts,
383
385
  });
384
386
  if (waiting)
385
387
  broadcastToWorkspace(workspaceId, 'channel.message.updated', waiting);
@@ -415,6 +417,24 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
415
417
  },
416
418
  error: result.error,
417
419
  });
420
+ const replyParts = buildContinuationParts(existingMessage?.parts, session.id, options.appendUserMessage, buildAgentMessageParts({
421
+ sessionId: session.id,
422
+ workspaceRoot: workspace?.boundDirs?.[0],
423
+ presetName: preset.name || preset.role,
424
+ role: preset.role,
425
+ model: preset.modelId,
426
+ usage: result.usage,
427
+ systemPrompt: preset.systemPrompt,
428
+ mcpServers: Object.keys(mcpServers ?? {}),
429
+ skills,
430
+ builtInTools: buildBuiltInTools(functionTools, channel, issue),
431
+ output: displayOutput,
432
+ reasoning: liveReasoning,
433
+ toolDetails,
434
+ askUserQuestions,
435
+ success: result.success,
436
+ error: result.error,
437
+ }));
418
438
  const reply = updateMessage(workspaceId, channelId, pending.id, {
419
439
  content: result.success ? displayOutput.join('\n') : result.error || displayOutput.join('\n'),
420
440
  type: 'text',
@@ -426,24 +446,7 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
426
446
  summary: result.summary,
427
447
  duration: Date.now() - startTime,
428
448
  },
429
- parts: buildAgentMessageParts({
430
- sessionId: session.id,
431
- workspaceRoot: workspace?.boundDirs?.[0],
432
- presetName: preset.name || preset.role,
433
- role: preset.role,
434
- model: preset.modelId,
435
- usage: result.usage,
436
- systemPrompt: preset.systemPrompt,
437
- mcpServers: Object.keys(mcpServers ?? {}),
438
- skills,
439
- builtInTools: buildBuiltInTools(functionTools, channel, issue),
440
- output: displayOutput,
441
- reasoning: liveReasoning,
442
- toolDetails,
443
- askUserQuestions,
444
- success: result.success,
445
- error: result.error,
446
- }),
449
+ parts: replyParts,
447
450
  });
448
451
  if (reply)
449
452
  broadcastToWorkspace(workspaceId, 'channel.message.updated', reply);
@@ -460,6 +463,23 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
460
463
  durationMs: Date.now() - startTime,
461
464
  });
462
465
  broadcastToWorkspace(workspaceId, 'agent.error', { agentId: session.id, error });
466
+ const errorParts = buildContinuationParts(existingMessage?.parts, session.id, options.appendUserMessage, buildAgentMessageParts({
467
+ sessionId: session.id,
468
+ workspaceRoot: workspace?.boundDirs?.[0],
469
+ presetName: preset.name || preset.role,
470
+ role: preset.role,
471
+ model: preset.modelId,
472
+ systemPrompt: preset.systemPrompt,
473
+ mcpServers: Object.keys(mcpServers ?? {}),
474
+ skills,
475
+ builtInTools: buildBuiltInTools(functionTools, channel, issue),
476
+ output: [error],
477
+ reasoning: liveReasoning,
478
+ toolDetails: new Map(),
479
+ askUserQuestions: [],
480
+ success: false,
481
+ error,
482
+ }));
463
483
  const reply = updateMessage(workspaceId, channelId, pending.id, {
464
484
  content: error,
465
485
  type: 'text',
@@ -470,23 +490,7 @@ export async function runMentionedAgent(workspaceId, channelId, agentConfigId, p
470
490
  model: preset.modelId,
471
491
  duration: Date.now() - startTime,
472
492
  },
473
- parts: buildAgentMessageParts({
474
- sessionId: session.id,
475
- workspaceRoot: workspace?.boundDirs?.[0],
476
- presetName: preset.name || preset.role,
477
- role: preset.role,
478
- model: preset.modelId,
479
- systemPrompt: preset.systemPrompt,
480
- mcpServers: Object.keys(mcpServers ?? {}),
481
- skills,
482
- builtInTools: buildBuiltInTools(functionTools, channel, issue),
483
- output: [error],
484
- reasoning: liveReasoning,
485
- toolDetails: new Map(),
486
- askUserQuestions: [],
487
- success: false,
488
- error,
489
- }),
493
+ parts: errorParts,
490
494
  });
491
495
  if (reply)
492
496
  broadcastToWorkspace(workspaceId, 'channel.message.updated', reply);
@@ -535,6 +539,17 @@ function untrackChannelRun(workspaceId, channelId, agentId) {
535
539
  if (runs.size === 0)
536
540
  activeChannelRuns.delete(key);
537
541
  }
542
+ function buildContinuationParts(previousParts, sessionId, userMessage, nextParts) {
543
+ if (!userMessage)
544
+ return nextParts;
545
+ const appended = [{
546
+ id: `user-message-${sessionId}`,
547
+ type: 'user_message',
548
+ text: userMessage,
549
+ senderName: '用户',
550
+ }, ...nextParts];
551
+ return [...(previousParts ?? []), ...appended];
552
+ }
538
553
  function questionRunKey(workspaceId, channelId, messageId, questionId) {
539
554
  return `${workspaceId}:${channelId}:${messageId}:${questionId}`;
540
555
  }
@@ -1,6 +1,7 @@
1
1
  import { addConnection, broadcastToWorkspace } from './connection-manager.js';
2
2
  import { handleTerminalEvent } from './terminal-handler.js';
3
- import { createMessage } from '../services/message.js';
3
+ import * as ptyService from '../services/pty.js';
4
+ import { appendMessageReply, createMessage } from '../services/message.js';
4
5
  import { getChannel } from '../services/channel.js';
5
6
  import * as agentService from '../services/agent.js';
6
7
  import { runMentionedAgent, stopChannelRuns, handleAnswerQuestion, ensureScheduler, makeContext } from './agent-runner.js';
@@ -19,6 +20,16 @@ export function handleConnection(ws, workspaceId) {
19
20
  timestamp: new Date().toISOString(),
20
21
  data: { workspaceId },
21
22
  }));
23
+ // Send existing terminal sessions for reconnection
24
+ const existingSessions = ptyService.getSessionsByWorkspace(workspaceId);
25
+ ws.send(JSON.stringify({
26
+ event: 'terminal.sessions',
27
+ workspaceId,
28
+ timestamp: new Date().toISOString(),
29
+ data: {
30
+ sessions: existingSessions.map(s => ({ sessionId: s.id, cwd: s.cwd, buffer: s.buffer.join('') })),
31
+ },
32
+ }));
22
33
  ws.on('message', (raw) => {
23
34
  try {
24
35
  const msg = JSON.parse(raw.toString());
@@ -49,11 +60,29 @@ for (const evt of terminalEvents) {
49
60
  }
50
61
  // Register channel handlers
51
62
  registerHandler('channel.message', (_ws, workspaceId, data) => {
52
- const { channelId, content, type, mentions, attachments } = data;
63
+ const { channelId, content, type, mentions, attachments, replyToMessageId } = data;
53
64
  if (!channelId || (!content && !attachments?.length))
54
65
  return;
55
66
  if (!getChannel(workspaceId, channelId))
56
67
  return;
68
+ if (replyToMessageId) {
69
+ const updated = appendMessageReply(workspaceId, channelId, replyToMessageId, {
70
+ senderId: 'user',
71
+ content,
72
+ attachments,
73
+ });
74
+ if (!updated)
75
+ return;
76
+ broadcastToWorkspace(workspaceId, 'channel.message.updated', updated);
77
+ const agentId = updated.senderId !== 'user' ? updated.senderId : undefined;
78
+ if (agentId) {
79
+ void runMentionedAgent(workspaceId, channelId, agentId, stripHtml(content), {
80
+ messageId: updated.id,
81
+ appendUserMessage: stripHtml(content),
82
+ });
83
+ }
84
+ return;
85
+ }
57
86
  const message = createMessage(workspaceId, channelId, {
58
87
  senderId: 'user',
59
88
  content,
@@ -254,7 +254,7 @@ function buildChainItems(lines, finalTextRange, finalText, workspaceRoot, toolDe
254
254
  }
255
255
  }
256
256
  flushMessageBuffer();
257
- return items.slice(0, 40);
257
+ return items;
258
258
  }
259
259
  function buildToolTodo(line, index, workspaceRoot, toolDetails, toolDetailMatchCounts) {
260
260
  const summary = summarizeToolLine(line, workspaceRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-spaces/server",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "agent-spaces-server": "./dist/app.js"