newmark-agent 0.4.6 → 0.4.8

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.
Files changed (43) hide show
  1. package/assets/app-icon-dark.svg +6 -0
  2. package/dist/assets/app-icon-dark.svg +6 -0
  3. package/dist/cli-commands.js +2 -1
  4. package/dist/cli-discovery.js +2 -0
  5. package/dist/conversation-utility-host.bundle.cjs +577 -166
  6. package/dist/conversation-utility-host.js +3 -0
  7. package/dist/core/agent.d.ts +40 -4
  8. package/dist/core/agent.js +251 -49
  9. package/dist/core/agentKernelRunner.d.ts +3 -0
  10. package/dist/core/agentKernelRunner.js +74 -91
  11. package/dist/core/config.js +1 -1
  12. package/dist/core/conversationKernel.d.ts +40 -0
  13. package/dist/core/conversationKernel.js +219 -8
  14. package/dist/core/electronUtilityAgentClient.d.ts +8 -0
  15. package/dist/core/electronUtilityAgentClient.js +5 -1
  16. package/dist/core/electronUtilityRuntimePool.d.ts +15 -0
  17. package/dist/core/electronUtilityRuntimePool.js +11 -0
  18. package/dist/core/installUpdate.js +11 -8
  19. package/dist/core/mobilePairing.d.ts +1 -0
  20. package/dist/core/mobilePairing.js +15 -1
  21. package/dist/core/subagent.d.ts +10 -3
  22. package/dist/core/subagent.js +23 -8
  23. package/dist/core/toolPolicy.js +14 -3
  24. package/dist/core/utilityAgentProtocol.d.ts +29 -1
  25. package/dist/core/utilityHostToolRouter.js +18 -0
  26. package/dist/core/wslAgentClient.d.ts +8 -0
  27. package/dist/core/wslAgentClient.js +5 -1
  28. package/dist/core/wslAgentProtocol.d.ts +18 -1
  29. package/dist/core/wslAgentRuntimePool.d.ts +15 -0
  30. package/dist/core/wslAgentRuntimePool.js +11 -0
  31. package/dist/launcher.js +14 -11
  32. package/dist/main.js +207 -9
  33. package/dist/providers/chat-completions.adapter.js +6 -2
  34. package/dist/providers/responses.adapter.js +1 -0
  35. package/dist/server.d.ts +33 -1
  36. package/dist/server.js +696 -48
  37. package/dist/toolchain/registry-seeder.js +3 -1
  38. package/dist/tools/index.js +57 -6
  39. package/dist/tools/nativeTools.js +2 -1
  40. package/dist/ui/index.html +88 -101
  41. package/dist/wsl-agent-host.bundle.cjs +577 -166
  42. package/dist/wsl-agent-host.js +3 -0
  43. package/package.json +4 -2
package/dist/server.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runServer = runServer;
37
+ exports.setHostedServerAutomation = setHostedServerAutomation;
37
38
  const http = __importStar(require("http"));
38
39
  const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
@@ -54,6 +55,28 @@ let automation = null;
54
55
  let workspaceFileRouter = null;
55
56
  let mobileToken = '';
56
57
  let appRoot = '';
58
+ const mobileWorkEventSubscribers = new Set();
59
+ const mobileScopedRuntimes = new Map();
60
+ let hostedWorkEventSink = null;
61
+ let unsubscribeHostedWorkEvents = null;
62
+ let hostedConversationUiState;
63
+ let hostedConversationUiAction;
64
+ let hostedConversationPrompt;
65
+ function mobileRuntimeKey(workspaceId, conversationId) {
66
+ return `${String(workspaceId || '')}::${String(conversationId || '')}`;
67
+ }
68
+ function publishMobileWorkEvent(event) {
69
+ for (const subscriber of mobileWorkEventSubscribers) {
70
+ try {
71
+ subscriber(event);
72
+ }
73
+ catch { /* ignore disconnected mobile listeners */ }
74
+ }
75
+ }
76
+ function publishServerWorkEvent(event) {
77
+ publishMobileWorkEvent(event);
78
+ hostedWorkEventSink?.(event);
79
+ }
57
80
  function mobileAuthorized(req) {
58
81
  if (!mobileToken)
59
82
  return false;
@@ -77,6 +100,103 @@ function mobileJson(res, data, code = 200) {
77
100
  });
78
101
  res.end(body);
79
102
  }
103
+ function resolveMobileWorkspace(current, workspaceId) {
104
+ const clean = String(workspaceId || '');
105
+ return [...current.workspace.internal, ...current.workspace.external].find(workspace => workspace.id === clean)
106
+ || (current.workspace.current?.id === clean ? current.workspace.current : null);
107
+ }
108
+ const MOBILE_EDITABLE_EXTENSIONS = new Set([
109
+ '.txt', '.md', '.markdown', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
110
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.htm', '.xml', '.svg',
111
+ '.kt', '.kts', '.java', '.py', '.rb', '.rs', '.go', '.c', '.h', '.cpp', '.hpp', '.cs', '.sh',
112
+ '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
113
+ ]);
114
+ const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
115
+ function resolveMobileWorkspacePath(ws, relativePath) {
116
+ const root = path.resolve(ws.path);
117
+ const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
118
+ const target = path.resolve(root, clean || '.');
119
+ if (target !== root && !target.startsWith(root + path.sep))
120
+ throw new Error('Path escapes workspace');
121
+ return { root, target, relative: path.relative(root, target).replace(/\\/g, '/') };
122
+ }
123
+ async function assertMobileRealPathContained(root, target) {
124
+ const realRoot = await fs.promises.realpath(root);
125
+ const realTarget = await fs.promises.realpath(target);
126
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep))
127
+ throw new Error('Path escapes workspace through a symbolic link');
128
+ }
129
+ function mobileEditableFile(target) {
130
+ const base = path.basename(target).toLowerCase();
131
+ return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
132
+ }
133
+ function mobileRightSidebarState(current, ws, conversationId) {
134
+ const scoped = mobileScopedAgent(ws, conversationId);
135
+ return {
136
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
137
+ conversationId,
138
+ conversationPlan: scoped.getConversationPlan(conversationId),
139
+ linkedPlan: scoped.getLinkedPlan(conversationId),
140
+ subagents: scoped.subagents.listAll()
141
+ .filter(record => !record.conversationId || record.conversationId === conversationId)
142
+ .map(record => ({
143
+ id: record.id,
144
+ name: record.name,
145
+ displayName: record.displayName,
146
+ status: record.status,
147
+ model: record.model,
148
+ mode: record.agentMode,
149
+ inputMode: record.inputMode,
150
+ result: record.result,
151
+ error: record.error || '',
152
+ messageCount: record.messages.length,
153
+ messages: record.messages.slice(-40),
154
+ })),
155
+ };
156
+ }
157
+ function mobileWorkspaceConversationRows(current, ws) {
158
+ const activeConversationId = current.activeConversationIdForWorkspace(ws);
159
+ const isRuntimeWorkspace = !!current.workspace.current
160
+ && path.resolve(current.workspace.current.path) === path.resolve(ws.path);
161
+ const activeRuntimeStatus = current.status === 'working' ? 'running' : current.status;
162
+ return current.listWorkspaceConversationStates(ws).map(conversation => {
163
+ const scopedRuntime = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversation.id));
164
+ const scopedStatus = scopedRuntime?.agent.status === 'working' ? 'running' : scopedRuntime?.agent.status || '';
165
+ const runtimeStatus = scopedRuntime
166
+ ? scopedStatus
167
+ : isRuntimeWorkspace && conversation.id === current.activeConversationId && activeRuntimeStatus !== 'idle'
168
+ ? activeRuntimeStatus
169
+ : '';
170
+ return {
171
+ ...conversation,
172
+ active: conversation.id === activeConversationId || !!scopedRuntime,
173
+ runtimeStatus,
174
+ running: ['running', 'stopping', 'force_restarting'].includes(runtimeStatus),
175
+ };
176
+ });
177
+ }
178
+ function mobileScopedAgent(ws, conversationId) {
179
+ const scoped = new agent_1.Agent(appRoot, {
180
+ agentOnly: true,
181
+ workspaceRegistryMode: 'detached',
182
+ readOnlyConfig: true,
183
+ conversationId,
184
+ });
185
+ scoped.workspace.current = { ...ws };
186
+ scoped.config.loadWorkspaceConfig(ws.path);
187
+ scoped.setConversationFromStorage(conversationId);
188
+ return scoped;
189
+ }
190
+ function mobileConversationRuntimeBusy(current, ws, conversationId) {
191
+ const scoped = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversationId));
192
+ if (scoped && scoped.agent.status !== 'idle')
193
+ return true;
194
+ const currentWs = current.workspace.current;
195
+ return !!currentWs
196
+ && path.resolve(currentWs.path) === path.resolve(ws.path)
197
+ && current.activeConversationId === conversationId
198
+ && current.status !== 'idle';
199
+ }
80
200
  function handleMobileEvents(req, res) {
81
201
  if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
82
202
  mobileJson(res, { error: 'Remote touch disabled' }, 403);
@@ -97,23 +217,24 @@ function handleMobileEvents(req, res) {
97
217
  'Access-Control-Allow-Origin': '*',
98
218
  });
99
219
  res.write('retry: 3000\n\n');
100
- const unsubscribe = agent.subscribeWorkEvents(event => {
220
+ const subscriber = (event) => {
101
221
  try {
102
222
  res.write(`event: work\ndata: ${JSON.stringify(event)}\n\n`);
103
223
  }
104
224
  catch {
105
225
  // socket is gone; the close handler will clean up
106
226
  }
107
- });
227
+ };
228
+ mobileWorkEventSubscribers.add(subscriber);
108
229
  const heartbeat = setInterval(() => {
109
230
  try {
110
231
  res.write(': ping\n\n');
111
232
  }
112
233
  catch { }
113
234
  }, 15000);
114
- req.on('close', () => {
235
+ res.on('close', () => {
115
236
  clearInterval(heartbeat);
116
- unsubscribe();
237
+ mobileWorkEventSubscribers.delete(subscriber);
117
238
  });
118
239
  }
119
240
  function resolveAppPath(root, targetPath) {
@@ -705,6 +826,7 @@ async function handleApi(req, res, body) {
705
826
  hostname: os.hostname(),
706
827
  platform: process.platform,
707
828
  tailscaleIpv4: (0, mobilePairing_1.tailscaleIpv4)(),
829
+ lanIpv4: (0, mobilePairing_1.lanIpv4)(),
708
830
  workspace: agent.workspace.current ? {
709
831
  id: agent.workspace.current.id,
710
832
  name: agent.workspace.current.name,
@@ -717,9 +839,17 @@ async function handleApi(req, res, body) {
717
839
  }
718
840
  case '/api/mobile/state': {
719
841
  const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
842
+ // 完整对话信息:workRuns 用持久化记录透出(含被中断的构建,不依赖运行时内存)
843
+ const persistedRuns = agent.getPersistedConversationWorkRuns(agent.activeConversationId);
844
+ if (persistedRuns.length)
845
+ active.workRuns = persistedRuns;
720
846
  mobileJson(res, {
721
847
  mode: agent.mode,
722
848
  model: agent.modelSelectionValue(),
849
+ modelLabel: agent.modelLabel(),
850
+ models: agent.allModelNames(),
851
+ providers: (0, config_1.sanitizeProvidersForState)(agent.config.providers()),
852
+ intelligence: agent.intelligence,
723
853
  status: agent.status,
724
854
  activeConversationId: agent.activeConversationId,
725
855
  conversations: agent.listConversationStates(),
@@ -729,6 +859,11 @@ async function handleApi(req, res, body) {
729
859
  chatMessages: active.chatMessages,
730
860
  totalMessages: active.totalMessages,
731
861
  conversationLocked: agent.isConversationLocked(),
862
+ // workRuns 透出:完整 Build Block 记录(含 interrupted/force_interrupted),供移动端渲染被中断的对话
863
+ workRuns: active.workRuns,
864
+ // goal bar / flow bar:目标状态与当前 Flow 选择
865
+ goal: active.goal,
866
+ flowSelection: active.flowSelection,
732
867
  });
733
868
  return;
734
869
  }
@@ -737,12 +872,37 @@ async function handleApi(req, res, body) {
737
872
  return;
738
873
  }
739
874
  case '/api/mobile/conversation': {
740
- const conversationId = url.searchParams.get('conversationId') || agent.activeConversationId;
875
+ const workspaceId = url.searchParams.get('workspaceId') || '';
741
876
  const windowParam = url.searchParams.get('window');
742
877
  const beforeParam = url.searchParams.get('before');
743
878
  const windowSize = windowParam ? Math.max(1, Math.min(500, Number(windowParam) || 200)) : 200;
744
879
  const before = beforeParam ? Math.max(0, Number(beforeParam) || 0) : undefined;
745
- const snapshot = agent.getConversationSnapshot(String(conversationId), { window: windowSize, before });
880
+ const current = agent;
881
+ const ws = workspaceId ? resolveMobileWorkspace(current, workspaceId) : null;
882
+ if (workspaceId && !ws) {
883
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
884
+ return;
885
+ }
886
+ const conversationId = url.searchParams.get('conversationId')
887
+ || (ws ? current.activeConversationIdForWorkspace(ws) : current.activeConversationId);
888
+ if (ws && !current.hasConversationInWorkspace(String(conversationId), ws)) {
889
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
890
+ return;
891
+ }
892
+ // GUI conversations normally execute inside isolated runtime pools.
893
+ // Their durable target state can be newer than the hosted main Agent's
894
+ // foreground memory, so workspace-bound mobile reads must hydrate a
895
+ // fresh read-only target Agent from storage.
896
+ const snapshotAgent = ws ? mobileScopedAgent(ws, String(conversationId)) : current;
897
+ const snapshot = snapshotAgent.getConversationSnapshot(String(conversationId), {
898
+ window: windowSize,
899
+ before,
900
+ workspace: ws || undefined,
901
+ });
902
+ // 完整对话信息:workRuns 统一用持久化记录透出(含被中断的构建)
903
+ const persistedRuns = snapshotAgent.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
904
+ if (persistedRuns.length)
905
+ snapshot.workRuns = persistedRuns;
746
906
  mobileJson(res, snapshot);
747
907
  return;
748
908
  }
@@ -750,6 +910,305 @@ async function handleApi(req, res, body) {
750
910
  mobileJson(res, { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current });
751
911
  return;
752
912
  }
913
+ case '/api/mobile/workspace-conversations': {
914
+ const workspaceId = url.searchParams.get('workspaceId') || '';
915
+ const current = agent;
916
+ const ws = resolveMobileWorkspace(current, workspaceId);
917
+ if (!ws) {
918
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
919
+ return;
920
+ }
921
+ const conversations = mobileWorkspaceConversationRows(current, ws);
922
+ mobileJson(res, { workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal }, conversations });
923
+ return;
924
+ }
925
+ case '/api/mobile/right-sidebar-state': {
926
+ const workspaceId = url.searchParams.get('workspaceId') || '';
927
+ const conversationId = url.searchParams.get('conversationId') || '';
928
+ const ws = resolveMobileWorkspace(agent, workspaceId);
929
+ if (!ws) {
930
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
931
+ return;
932
+ }
933
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
934
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
935
+ return;
936
+ }
937
+ mobileJson(res, mobileRightSidebarState(agent, ws, conversationId));
938
+ return;
939
+ }
940
+ case '/api/mobile/conversation-ui-state': {
941
+ const workspaceId = url.searchParams.get('workspaceId') || '';
942
+ const conversationId = url.searchParams.get('conversationId') || '';
943
+ const ws = resolveMobileWorkspace(agent, workspaceId);
944
+ if (!ws) {
945
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
946
+ return;
947
+ }
948
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
949
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
950
+ return;
951
+ }
952
+ if (hostedConversationUiState) {
953
+ mobileJson(res, await hostedConversationUiState({ workspaceId, conversationId }));
954
+ return;
955
+ }
956
+ const scoped = mobileScopedAgent(ws, conversationId);
957
+ const snapshot = scoped.getConversationSnapshot(conversationId, { workspace: ws, window: 1 });
958
+ mobileJson(res, {
959
+ goal: snapshot.goal,
960
+ flowSelection: snapshot.flowSelection,
961
+ flow: null,
962
+ queued: { steering: [], followUp: [] },
963
+ runtime: null,
964
+ mode: scoped.mode,
965
+ status: scoped.status,
966
+ inputMode: scoped.inputMode,
967
+ });
968
+ return;
969
+ }
970
+ case '/api/mobile/workspace-files': {
971
+ const workspaceId = url.searchParams.get('workspaceId') || '';
972
+ const relativePath = url.searchParams.get('path') || '';
973
+ const ws = resolveMobileWorkspace(agent, workspaceId);
974
+ if (!ws) {
975
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
976
+ return;
977
+ }
978
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
979
+ await assertMobileRealPathContained(resolved.root, resolved.target);
980
+ const stat = await fs.promises.stat(resolved.target);
981
+ if (!stat.isDirectory()) {
982
+ mobileJson(res, { error: 'Path is not a directory' }, 400);
983
+ return;
984
+ }
985
+ const entries = await fs.promises.readdir(resolved.target, { withFileTypes: true });
986
+ const rows = entries
987
+ .filter(entry => entry.name !== '.git' && entry.name !== 'node_modules')
988
+ .map(entry => ({
989
+ name: entry.name,
990
+ path: path.posix.join(resolved.relative, entry.name).replace(/^\.\//, ''),
991
+ directory: entry.isDirectory(),
992
+ }))
993
+ .sort((a, b) => Number(b.directory) - Number(a.directory) || a.name.localeCompare(b.name));
994
+ mobileJson(res, { workspaceId, path: resolved.relative, entries: rows });
995
+ return;
996
+ }
997
+ case '/api/mobile/workspace-file': {
998
+ if (req.method === 'GET') {
999
+ const workspaceId = url.searchParams.get('workspaceId') || '';
1000
+ const relativePath = url.searchParams.get('path') || '';
1001
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1002
+ if (!ws) {
1003
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1004
+ return;
1005
+ }
1006
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
1007
+ await assertMobileRealPathContained(resolved.root, resolved.target);
1008
+ const stat = await fs.promises.stat(resolved.target);
1009
+ if (!stat.isFile() || stat.size > MOBILE_EDITOR_MAX_BYTES || !mobileEditableFile(resolved.target)) {
1010
+ mobileJson(res, { error: 'File is not an editable text file' }, 415);
1011
+ return;
1012
+ }
1013
+ const content = (await fs.promises.readFile(resolved.target, 'utf-8')).replace(/^\uFEFF/, '');
1014
+ mobileJson(res, { workspaceId, path: resolved.relative, content, size: stat.size });
1015
+ return;
1016
+ }
1017
+ const params = JSON.parse(body || '{}');
1018
+ const workspaceId = String(params.workspaceId || '');
1019
+ const relativePath = String(params.path || '');
1020
+ const content = String(params.content ?? '');
1021
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1022
+ if (!ws) {
1023
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1024
+ return;
1025
+ }
1026
+ if (Buffer.byteLength(content, 'utf-8') > MOBILE_EDITOR_MAX_BYTES) {
1027
+ mobileJson(res, { error: 'File exceeds mobile editor size limit' }, 413);
1028
+ return;
1029
+ }
1030
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
1031
+ await assertMobileRealPathContained(resolved.root, resolved.target);
1032
+ if (!mobileEditableFile(resolved.target)) {
1033
+ mobileJson(res, { error: 'File type is not editable' }, 415);
1034
+ return;
1035
+ }
1036
+ const stat = await fs.promises.stat(resolved.target);
1037
+ if (!stat.isFile()) {
1038
+ mobileJson(res, { error: 'Path is not a file' }, 400);
1039
+ return;
1040
+ }
1041
+ await fs.promises.writeFile(resolved.target, content, 'utf-8');
1042
+ mobileJson(res, { ok: true, workspaceId, path: resolved.relative, size: Buffer.byteLength(content, 'utf-8') });
1043
+ return;
1044
+ }
1045
+ case '/api/mobile/conversation-plan-update': {
1046
+ const params = JSON.parse(body || '{}');
1047
+ const workspaceId = String(params.workspaceId || '');
1048
+ const conversationId = String(params.conversationId || '');
1049
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1050
+ if (!ws) {
1051
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1052
+ return;
1053
+ }
1054
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
1055
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1056
+ return;
1057
+ }
1058
+ const scoped = mobileScopedAgent(ws, conversationId);
1059
+ const plan = scoped.updateConversationPlan({ items: Array.isArray(params.items) ? params.items : [] }, conversationId);
1060
+ mobileJson(res, { ok: true, conversationPlan: plan });
1061
+ return;
1062
+ }
1063
+ case '/api/mobile/conversation-create': {
1064
+ const params = JSON.parse(body || '{}');
1065
+ const workspaceId = String(params.workspaceId || '');
1066
+ if (!workspaceId) {
1067
+ mobileJson(res, { error: 'No workspaceId' }, 400);
1068
+ return;
1069
+ }
1070
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1071
+ if (!ws) {
1072
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1073
+ return;
1074
+ }
1075
+ const created = agent.createConversationInWorkspace(ws, String(params.title || ''));
1076
+ mobileJson(res, {
1077
+ ok: true,
1078
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
1079
+ conversation: created,
1080
+ activeConversationId: created.id,
1081
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1082
+ });
1083
+ return;
1084
+ }
1085
+ case '/api/mobile/model': {
1086
+ const next = String(JSON.parse(body || '{}').model || '');
1087
+ if (!next) {
1088
+ mobileJson(res, { error: 'Model is required' }, 400);
1089
+ return;
1090
+ }
1091
+ agent.setModel(next);
1092
+ mobileJson(res, {
1093
+ ok: true,
1094
+ model: agent.modelSelectionValue(),
1095
+ modelLabel: agent.modelLabel(),
1096
+ });
1097
+ return;
1098
+ }
1099
+ case '/api/mobile/intelligence': {
1100
+ const tier = String(JSON.parse(body || '{}').tier || 'medium');
1101
+ agent.setIntelligence(tier, true);
1102
+ mobileJson(res, { ok: true, intelligence: agent.intelligence });
1103
+ return;
1104
+ }
1105
+ case '/api/mobile/conversation-ui-action': {
1106
+ const params = JSON.parse(body || '{}');
1107
+ const workspaceId = String(params.workspaceId || '');
1108
+ const conversationId = String(params.conversationId || '');
1109
+ const action = String(params.action || '');
1110
+ const allowed = new Set(['goal_update', 'goal_guide', 'conversation_guide', 'goal_toggle_pause', 'goal_clear', 'flow_pause', 'flow_resume', 'flow_guide', 'conversation_stop', 'input_mode', 'queue_enqueue', 'queue_update', 'queue_delete', 'queue_toggle_pause', 'queue_guide']);
1111
+ if (!workspaceId || !conversationId || !allowed.has(action)) {
1112
+ mobileJson(res, { error: 'workspaceId, conversationId, and a valid action are required' }, 400);
1113
+ return;
1114
+ }
1115
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1116
+ if (!ws) {
1117
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1118
+ return;
1119
+ }
1120
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1121
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1122
+ return;
1123
+ }
1124
+ if (!hostedConversationUiAction) {
1125
+ mobileJson(res, { error: 'This action requires the GUI-hosted mobile server' }, 409);
1126
+ return;
1127
+ }
1128
+ mobileJson(res, await hostedConversationUiAction({ workspaceId, conversationId }, action, String(params.value || ''), params));
1129
+ return;
1130
+ }
1131
+ case '/api/mobile/conversation-rename': {
1132
+ const params = JSON.parse(body || '{}');
1133
+ const workspaceId = String(params.workspaceId || '');
1134
+ const conversationId = String(params.conversationId || '');
1135
+ const title = String(params.title || '');
1136
+ if (!workspaceId || !conversationId || !title.trim()) {
1137
+ mobileJson(res, { error: 'workspaceId, conversationId, and title are required' }, 400);
1138
+ return;
1139
+ }
1140
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1141
+ if (!ws) {
1142
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1143
+ return;
1144
+ }
1145
+ const ok = agent.renameConversation(conversationId, title, ws);
1146
+ if (!ok) {
1147
+ mobileJson(res, { ok: false, error: 'Conversation rename failed' }, 404);
1148
+ return;
1149
+ }
1150
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(agent, ws) });
1151
+ return;
1152
+ }
1153
+ case '/api/mobile/conversation-pin': {
1154
+ const params = JSON.parse(body || '{}');
1155
+ const workspaceId = String(params.workspaceId || '');
1156
+ const conversationId = String(params.conversationId || '');
1157
+ if (!workspaceId || !conversationId) {
1158
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1159
+ return;
1160
+ }
1161
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1162
+ if (!ws) {
1163
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1164
+ return;
1165
+ }
1166
+ const pinned = params.pinned === true;
1167
+ const ok = agent.setConversationPinned(conversationId, pinned, ws);
1168
+ if (!ok) {
1169
+ mobileJson(res, { ok: false, error: 'Conversation pin update failed' }, 404);
1170
+ return;
1171
+ }
1172
+ mobileJson(res, { ok: true, pinned, conversations: mobileWorkspaceConversationRows(agent, ws) });
1173
+ return;
1174
+ }
1175
+ case '/api/mobile/conversation-reorder': {
1176
+ const params = JSON.parse(body || '{}');
1177
+ const workspaceId = String(params.workspaceId || '');
1178
+ const conversationIds = Array.isArray(params.conversationIds)
1179
+ ? params.conversationIds.map((id) => String(id || ''))
1180
+ : [];
1181
+ if (!workspaceId || conversationIds.length < 2 || conversationIds.some((id) => !id)) {
1182
+ mobileJson(res, { error: 'workspaceId and at least two conversationIds are required' }, 400);
1183
+ return;
1184
+ }
1185
+ if (new Set(conversationIds).size !== conversationIds.length) {
1186
+ mobileJson(res, { error: 'conversationIds must be unique' }, 400);
1187
+ return;
1188
+ }
1189
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1190
+ if (!ws) {
1191
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1192
+ return;
1193
+ }
1194
+ const current = agent;
1195
+ if (conversationIds.some((id) => !current.hasConversationInWorkspace(id, ws))) {
1196
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1197
+ return;
1198
+ }
1199
+ const listedById = new Map(current.listWorkspaceConversationStates(ws).map(item => [item.id, item]));
1200
+ if (new Set(conversationIds.map((id) => !!listedById.get(id)?.pinned)).size !== 1) {
1201
+ mobileJson(res, { error: 'Conversations must remain within one pinned group' }, 409);
1202
+ return;
1203
+ }
1204
+ const ok = current.reorderWorkspaceConversationGroup(conversationIds, ws);
1205
+ if (!ok) {
1206
+ mobileJson(res, { ok: false, error: 'Conversation reorder failed' }, 409);
1207
+ return;
1208
+ }
1209
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(current, ws) });
1210
+ return;
1211
+ }
753
1212
  case '/api/mobile/send': {
754
1213
  const params = JSON.parse(body || '{}');
755
1214
  const message = String(params.message || '');
@@ -757,23 +1216,188 @@ async function handleApi(req, res, body) {
757
1216
  mobileJson(res, { error: 'No message' }, 400);
758
1217
  return;
759
1218
  }
760
- if (params.conversationId)
761
- agent.setConversation(String(params.conversationId));
762
- const tokens = await agent.process(message);
763
- const snapshot = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
1219
+ const workspaceId = String(params.workspaceId || '');
1220
+ let requestAgent = agent;
1221
+ let requestWorkspace = agent.workspace.current;
1222
+ let resolvedWorkspace = null;
1223
+ if (workspaceId) {
1224
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1225
+ if (!ws) {
1226
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1227
+ return;
1228
+ }
1229
+ resolvedWorkspace = ws;
1230
+ requestWorkspace = ws;
1231
+ }
1232
+ const conversationId = String(params.conversationId
1233
+ || (resolvedWorkspace ? agent.activeConversationIdForWorkspace(resolvedWorkspace) : agent.activeConversationId));
1234
+ if (resolvedWorkspace) {
1235
+ const ws = resolvedWorkspace;
1236
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1237
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1238
+ return;
1239
+ }
1240
+ const currentWs = agent.workspace.current;
1241
+ if (!currentWs || path.resolve(currentWs.path) !== path.resolve(ws.path)) {
1242
+ requestAgent = mobileScopedAgent(ws, conversationId);
1243
+ }
1244
+ }
1245
+ if (hostedConversationPrompt && resolvedWorkspace) {
1246
+ const result = await hostedConversationPrompt({ workspaceId, conversationId }, message, {
1247
+ requestedMode: String(params.requestedMode || ''),
1248
+ goalObjective: String(params.goalObjective || ''),
1249
+ inputMode: String(params.inputMode || ''),
1250
+ });
1251
+ mobileJson(res, { ok: true, conversationId, ...result });
1252
+ return;
1253
+ }
1254
+ if (requestAgent === agent && params.conversationId)
1255
+ requestAgent.setConversation(conversationId);
1256
+ let scopedRuntimeKey = '';
1257
+ let scopedRuntime = null;
1258
+ let unsubscribeScoped = null;
1259
+ if (requestAgent !== agent && requestWorkspace) {
1260
+ scopedRuntimeKey = mobileRuntimeKey(String(requestWorkspace.id || ''), conversationId);
1261
+ unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishServerWorkEvent({
1262
+ ...event,
1263
+ workspaceId: String(requestWorkspace?.id || ''),
1264
+ conversationId,
1265
+ }));
1266
+ scopedRuntime = {
1267
+ agent: requestAgent,
1268
+ workspaceId: String(requestWorkspace.id || ''),
1269
+ conversationId,
1270
+ unsubscribe: unsubscribeScoped,
1271
+ };
1272
+ mobileScopedRuntimes.set(scopedRuntimeKey, scopedRuntime);
1273
+ }
1274
+ let tokens;
1275
+ try {
1276
+ tokens = await requestAgent.process(message);
1277
+ }
1278
+ finally {
1279
+ if (scopedRuntimeKey && mobileScopedRuntimes.get(scopedRuntimeKey) === scopedRuntime) {
1280
+ mobileScopedRuntimes.delete(scopedRuntimeKey);
1281
+ }
1282
+ if (unsubscribeScoped)
1283
+ unsubscribeScoped();
1284
+ }
1285
+ const snapshot = requestAgent.getConversationSnapshot(conversationId, {
1286
+ window: 200,
1287
+ workspace: requestWorkspace,
1288
+ });
764
1289
  mobileJson(res, {
765
1290
  ok: true,
766
- conversationId: agent.activeConversationId,
1291
+ conversationId,
767
1292
  response: tokens.map(token => token.text).join(''),
768
1293
  tokens: tokens.map(token => ({ type: token.type, text: token.text })),
769
- options: agent.pendingOptions,
770
- status: agent.status,
771
- conversations: agent.listConversationStates(),
1294
+ options: requestAgent.pendingOptions,
1295
+ status: requestAgent.status,
1296
+ conversations: requestWorkspace
1297
+ ? mobileWorkspaceConversationRows(requestAgent, requestWorkspace)
1298
+ : requestAgent.listConversationStates(),
772
1299
  chatMessages: snapshot.chatMessages,
773
1300
  totalMessages: snapshot.totalMessages,
774
1301
  });
775
1302
  return;
776
1303
  }
1304
+ case '/api/mobile/conversation-branch-inspect':
1305
+ case '/api/mobile/conversation-branch-activate':
1306
+ case '/api/mobile/conversation-branch-create': {
1307
+ const params = JSON.parse(body || '{}');
1308
+ const workspaceId = String(params.workspaceId || '');
1309
+ const conversationId = String(params.conversationId || '');
1310
+ if (!workspaceId || !conversationId) {
1311
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1312
+ return;
1313
+ }
1314
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1315
+ if (!ws) {
1316
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1317
+ return;
1318
+ }
1319
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1320
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1321
+ return;
1322
+ }
1323
+ const scoped = mobileScopedAgent(ws, conversationId);
1324
+ if (pathname === '/api/mobile/conversation-branch-inspect') {
1325
+ const branchId = String(params.branchId || '');
1326
+ if (!branchId) {
1327
+ mobileJson(res, { error: 'branchId is required' }, 400);
1328
+ return;
1329
+ }
1330
+ mobileJson(res, scoped.inspectConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1331
+ return;
1332
+ }
1333
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1334
+ mobileJson(res, { error: 'Conversation is running' }, 423);
1335
+ return;
1336
+ }
1337
+ if (pathname === '/api/mobile/conversation-branch-activate') {
1338
+ const branchId = String(params.branchId || '');
1339
+ if (!branchId) {
1340
+ mobileJson(res, { error: 'branchId is required' }, 400);
1341
+ return;
1342
+ }
1343
+ mobileJson(res, scoped.switchConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1344
+ return;
1345
+ }
1346
+ const messageIndex = Math.floor(Number(params.messageIndex));
1347
+ const editedText = String(params.editedText || '').trim();
1348
+ if (!Number.isFinite(messageIndex) || messageIndex < 0 || !editedText) {
1349
+ mobileJson(res, { error: 'messageIndex and editedText are required' }, 400);
1350
+ return;
1351
+ }
1352
+ const branchNodePath = Array.isArray(params.branchNodePath)
1353
+ ? params.branchNodePath.map((id) => String(id || '')).filter(Boolean)
1354
+ : [];
1355
+ const snapshot = scoped.branchConversation(conversationId, messageIndex, editedText, {
1356
+ messageId: String(params.messageId || '') || undefined,
1357
+ guideId: String(params.guideId || '') || undefined,
1358
+ clientMessageId: String(params.clientMessageId || '') || undefined,
1359
+ runId: String(params.runId || '') || undefined,
1360
+ branchNodePath,
1361
+ });
1362
+ mobileJson(res, snapshot);
1363
+ return;
1364
+ }
1365
+ case '/api/mobile/conversation-archive': {
1366
+ const params = JSON.parse(body || '{}');
1367
+ const conversationId = String(params.conversationId || '');
1368
+ const workspaceId = String(params.workspaceId || '');
1369
+ if (!workspaceId || !conversationId) {
1370
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1371
+ return;
1372
+ }
1373
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1374
+ if (!ws) {
1375
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1376
+ return;
1377
+ }
1378
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1379
+ mobileJson(res, { ok: false, error: 'Conversation not found in workspace' }, 404);
1380
+ return;
1381
+ }
1382
+ // 运行中拒绝:与移动端 running 判定一致(activeConversationId + agent.status 非 idle)
1383
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1384
+ mobileJson(res, { ok: false, error: 'Conversation is running' }, 423);
1385
+ return;
1386
+ }
1387
+ const filename = await agent.archiveConversationAsync(conversationId, ws);
1388
+ if (!filename) {
1389
+ mobileJson(res, { ok: false, error: 'Conversation archive could not be written.' }, 500);
1390
+ return;
1391
+ }
1392
+ mobileJson(res, {
1393
+ ok: true,
1394
+ fileName: filename,
1395
+ conversationId,
1396
+ activeConversationId: agent.activeConversationIdForWorkspace(ws),
1397
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1398
+ });
1399
+ return;
1400
+ }
777
1401
  default:
778
1402
  jsonResponse(res, { error: 'Unknown API' }, 404);
779
1403
  }
@@ -785,39 +1409,51 @@ async function handleApi(req, res, body) {
785
1409
  jsonResponse(res, { error: e.message }, 500);
786
1410
  }
787
1411
  }
788
- function startServer(root) {
1412
+ function startServer(root, options = {}) {
789
1413
  mobileToken = (0, mobilePairing_1.ensureMobileToken)(root);
790
1414
  appRoot = root;
791
- agent = new agent_1.Agent(root);
1415
+ agent = options.agent || new agent_1.Agent(root);
1416
+ hostedWorkEventSink = options.onWorkEvent || null;
1417
+ hostedConversationUiState = options.conversationUiState;
1418
+ hostedConversationUiAction = options.conversationUiAction;
1419
+ hostedConversationPrompt = options.conversationPrompt;
1420
+ unsubscribeHostedWorkEvents?.();
1421
+ unsubscribeHostedWorkEvents = options.subscribeWorkEvents?.(publishMobileWorkEvent) || null;
792
1422
  workspaceFileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
793
- automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
794
- if (!agent)
795
- return '';
796
- const previousModel = agent.model;
797
- const previousWorkspace = agent.workspace.current?.id || agent.workspace.current?.path || '';
798
- const previousConversation = agent.activeConversationId;
799
- agent.selectWorkspaceFromStorage(item.workspaceId);
800
- const conversationId = item.conversationMode === 'existing'
801
- ? item.conversationId
802
- : `automation-${item.id}-${Date.now().toString(36)}`;
803
- agent.setConversationFromStorage(conversationId);
804
- automation?.update(item.id, { lastConversationId: conversationId });
805
- if (model)
806
- agent.setModel(model);
807
- try {
808
- const tokens = await agent.process(prompt);
809
- return tokens.map(t => t.text).join('');
810
- }
811
- finally {
1423
+ automation = options.automation || null;
1424
+ if (!automation && !options.agent)
1425
+ automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
1426
+ if (!agent)
1427
+ return '';
1428
+ const previousModel = agent.model;
1429
+ const previousWorkspace = agent.workspace.current?.id || agent.workspace.current?.path || '';
1430
+ const previousConversation = agent.activeConversationId;
1431
+ agent.selectWorkspaceFromStorage(item.workspaceId);
1432
+ const conversationId = item.conversationMode === 'existing'
1433
+ ? item.conversationId
1434
+ : `automation-${item.id}-${Date.now().toString(36)}`;
1435
+ agent.setConversationFromStorage(conversationId);
1436
+ automation?.update(item.id, { lastConversationId: conversationId });
812
1437
  if (model)
813
- agent.setModel(previousModel);
814
- if (previousWorkspace)
815
- agent.selectWorkspaceFromStorage(previousWorkspace);
816
- agent.setConversationFromStorage(previousConversation);
817
- }
818
- });
819
- agent.setAutomationManager(automation);
820
- automation.start();
1438
+ agent.setModel(model);
1439
+ try {
1440
+ const tokens = await agent.process(prompt);
1441
+ return tokens.map(t => t.text).join('');
1442
+ }
1443
+ finally {
1444
+ if (model)
1445
+ agent.setModel(previousModel);
1446
+ if (previousWorkspace)
1447
+ agent.selectWorkspaceFromStorage(previousWorkspace);
1448
+ agent.setConversationFromStorage(previousConversation);
1449
+ }
1450
+ });
1451
+ agent.subscribeWorkEvents(publishServerWorkEvent);
1452
+ if (automation) {
1453
+ agent.setAutomationManager(automation);
1454
+ if (!options.automation)
1455
+ automation.start();
1456
+ }
821
1457
  const uiDir = path.join(__dirname, 'ui');
822
1458
  const server = http.createServer(async (req, res) => {
823
1459
  if (req.method === 'OPTIONS') {
@@ -853,19 +1489,31 @@ function startServer(root) {
853
1489
  });
854
1490
  const bindHost = process.env.NEWMARK_BIND_HOST || '0.0.0.0';
855
1491
  const tailscale = (0, mobilePairing_1.tailscaleIpv4)();
1492
+ const lan = (0, mobilePairing_1.lanIpv4)();
1493
+ const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
856
1494
  const tokenPath = path.join(root, '.newmark-mobile-token');
857
1495
  server.listen(PORT, bindHost, () => {
858
1496
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
859
1497
  console.log(` Bind: ${bindHost}:${PORT}`);
860
1498
  console.log(` GUI: http://localhost:${PORT}`);
861
- console.log(` Tailscale IPv4: ${tailscale || 'not detected (install/start Tailscale)'}`);
862
- console.log(` Mobile endpoint: http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/hello?token=<token>`);
1499
+ console.log(` Tailscale IPv4: ${tailscale || 'not detected'}`);
1500
+ console.log(` LAN IPv4: ${lan || 'not detected'}`);
1501
+ console.log(` Mobile endpoint: http://${accessHost}:${PORT}/api/mobile/hello?token=<token>`);
863
1502
  console.log(` Mobile token file: ${tokenPath}`);
864
- console.log(` Mobile events (SSE): http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/events?token=<token>`);
1503
+ console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
865
1504
  console.log(` Press Ctrl+C to stop\n`);
866
1505
  });
867
1506
  }
868
- function runServer(root) {
869
- startServer(root);
1507
+ let hostedServerStarted = false;
1508
+ /** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
1509
+ function runServer(root, options = {}) {
1510
+ if (hostedServerStarted)
1511
+ return;
1512
+ hostedServerStarted = true;
1513
+ startServer(root, options);
1514
+ }
1515
+ /** Attach the GUI-owned automation manager after deferred startup. */
1516
+ function setHostedServerAutomation(manager) {
1517
+ automation = manager;
870
1518
  }
871
1519
  //# sourceMappingURL=server.js.map