loadtoagent 1.1.0 → 1.3.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.ko.md +7 -0
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +23 -1
- package/docs/UI-AUDIT-100.md +118 -0
- package/docs/UI-AUDIT-101-200.md +120 -0
- package/docs/UI-AUDIT-201-300.md +120 -0
- package/main.js +135 -38
- package/package.json +13 -4
- package/preload.js +6 -0
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -19
- package/renderer/app-dashboard.js +190 -55
- package/renderer/app-drawer-content.js +48 -44
- package/renderer/app-drawer-data.js +26 -13
- package/renderer/app-drawer.js +72 -54
- package/renderer/app-events-dialogs.js +146 -37
- package/renderer/app-events-filters.js +172 -13
- package/renderer/app-events-navigation.js +77 -30
- package/renderer/app-events-sessions.js +156 -9
- package/renderer/app-events.js +2 -1
- package/renderer/app-graph-model.js +20 -38
- package/renderer/app-graph-orchestration.js +15 -13
- package/renderer/app-graph-view.js +228 -113
- package/renderer/app-management.js +211 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +103 -33
- package/renderer/app-runtime-overview.js +312 -0
- package/renderer/app-session-render.js +94 -37
- package/renderer/app-tmux-render.js +52 -44
- package/renderer/app.js +153 -75
- package/renderer/i18n-messages.js +833 -16
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +145 -59
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +6 -0
- package/renderer/styles-cards.css +26 -0
- package/renderer/styles-components.css +86 -9
- package/renderer/styles-management.css +355 -0
- package/renderer/styles-overlays.css +8 -2
- package/renderer/styles-product.css +24 -16
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +862 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +22 -14
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +37 -3
- package/renderer/styles-run-composer.css +5 -0
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +160 -0
- package/renderer/styles-terminal.css +339 -2
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +69 -2
- package/renderer/styles.css +27 -12
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +233 -40
- package/renderer/terminal-workbench.js +165 -77
- package/renderer/terminal.js +341 -34
- package/src/agentMonitor/claudeParser.js +96 -7
- package/src/agentMonitor/codexParser.js +59 -4
- package/src/agentMonitor/executionActivity.js +282 -0
- package/src/agentMonitor/genericParser.js +56 -13
- package/src/agentMonitor/hierarchy.js +17 -0
- package/src/agentMonitor/responseIntent.js +51 -0
- package/src/agentMonitor.js +21 -3
- package/src/agentRunner.js +66 -1
- package/src/attentionNotifier.js +14 -10
- package/src/automationMonitor.js +258 -0
- package/src/contracts.js +10 -1
- package/src/ipc/registerAgentIpc.js +9 -3
- package/src/ipc/registerAppIpc.js +4 -1
- package/src/ipc/registerTerminalIpc.js +12 -6
- package/src/macUpdateHelper.js +210 -0
- package/src/monitorWorker.js +65 -6
- package/src/platformPath.js +80 -0
- package/src/processMonitor.js +80 -22
- package/src/providerVisibilityStore.js +45 -0
- package/src/sessionIntelligence.js +247 -0
- package/src/terminalHost.js +381 -0
- package/src/terminalHostDaemon.js +60 -0
- package/src/terminalManager.js +158 -3
- package/src/updateInstaller.js +175 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { createExecutionTracker } = require('./executionActivity');
|
|
4
5
|
|
|
5
6
|
const TOOL_START_PATTERN = /tool_use|tool-call|tool_start/;
|
|
6
7
|
const TOOL_END_PATTERN = /tool_result|tool-result|tool_end/;
|
|
@@ -22,6 +23,8 @@ function createGenericParser(dependencies) {
|
|
|
22
23
|
sumUsage,
|
|
23
24
|
timestamp,
|
|
24
25
|
trimSession,
|
|
26
|
+
assistantRequestsUserResponse,
|
|
27
|
+
isUserInputTool,
|
|
25
28
|
} = dependencies;
|
|
26
29
|
|
|
27
30
|
function normalizeUsage(raw = {}) {
|
|
@@ -72,6 +75,7 @@ function createGenericParser(dependencies) {
|
|
|
72
75
|
const session = baseSession(provider, externalId, fileInfo.file, fileInfo);
|
|
73
76
|
session.truncated = parsed.truncated;
|
|
74
77
|
session.cwd = root.cwd || root.projectPath || root.project_path || '';
|
|
78
|
+
session.originCwd = session.cwd;
|
|
75
79
|
session.model = root.model || root.modelId || '';
|
|
76
80
|
session.startedAt = timestamp(root.startTime || root.startedAt || root.created_at, session.updatedAt);
|
|
77
81
|
session.parentId = root.parent_session_id ? `${provider}:${root.parent_session_id}` : null;
|
|
@@ -79,8 +83,12 @@ function createGenericParser(dependencies) {
|
|
|
79
83
|
return { rows, root, session };
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
function recordToolStart(session, event) {
|
|
86
|
+
function recordToolStart(session, state, event) {
|
|
83
87
|
const tool = event.tool_name || event.name || event.tool || 'tool';
|
|
88
|
+
const callId = event.tool_call_id || event.tool_use_id || event.id;
|
|
89
|
+
const args = event.parameters || event.args || event.input || {};
|
|
90
|
+
state.toolCalls.set(String(callId || ''), { name: tool, args });
|
|
91
|
+
state.executionTracker.recordCall({ name: tool, callId, args, rawInput: args, at: event.timestamp });
|
|
84
92
|
addMessage(session, {
|
|
85
93
|
id: event.id,
|
|
86
94
|
role: 'tool',
|
|
@@ -99,8 +107,17 @@ function createGenericParser(dependencies) {
|
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
function recordToolEnd(session, event) {
|
|
110
|
+
function recordToolEnd(session, state, event) {
|
|
103
111
|
const callId = event.tool_call_id || event.tool_use_id || event.id;
|
|
112
|
+
const call = state.toolCalls.get(String(callId || ''));
|
|
113
|
+
state.executionTracker.recordOutput({
|
|
114
|
+
name: call && call.name,
|
|
115
|
+
callId,
|
|
116
|
+
args: call && call.args || {},
|
|
117
|
+
output: event.output || event.result || event.content || event,
|
|
118
|
+
at: event.timestamp,
|
|
119
|
+
isError: Boolean(event.error),
|
|
120
|
+
});
|
|
104
121
|
settleLifecycle(session, callId, event.error ? 'failed' : 'done', event.timestamp);
|
|
105
122
|
addLifecycle(session, {
|
|
106
123
|
id: `result:${event.id || event.tool_call_id}`,
|
|
@@ -112,7 +129,13 @@ function createGenericParser(dependencies) {
|
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
function processEvents(session, events) {
|
|
115
|
-
const state = {
|
|
132
|
+
const state = {
|
|
133
|
+
running: false,
|
|
134
|
+
failed: false,
|
|
135
|
+
pendingUserInputCalls: new Set(),
|
|
136
|
+
toolCalls: new Map(),
|
|
137
|
+
executionTracker: createExecutionTracker({ compactText, timestamp }),
|
|
138
|
+
};
|
|
116
139
|
for (const event of events) {
|
|
117
140
|
const type = String(event.type || event.event || event.kind || '').toLowerCase();
|
|
118
141
|
if (type === 'init') {
|
|
@@ -127,10 +150,15 @@ function createGenericParser(dependencies) {
|
|
|
127
150
|
});
|
|
128
151
|
}
|
|
129
152
|
if (TOOL_START_PATTERN.test(type)) {
|
|
130
|
-
recordToolStart(session, event);
|
|
153
|
+
recordToolStart(session, state, event);
|
|
131
154
|
state.running = true;
|
|
155
|
+
const toolName = event.tool_name || event.name || event.tool;
|
|
156
|
+
if (isUserInputTool(toolName)) state.pendingUserInputCalls.add(String(event.id || toolName));
|
|
157
|
+
}
|
|
158
|
+
if (TOOL_END_PATTERN.test(type)) {
|
|
159
|
+
recordToolEnd(session, state, event);
|
|
160
|
+
state.pendingUserInputCalls.delete(String(event.tool_call_id || event.tool_use_id || event.id || ''));
|
|
132
161
|
}
|
|
133
|
-
if (TOOL_END_PATTERN.test(type)) recordToolEnd(session, event);
|
|
134
162
|
if (type === 'result' || /session_end|completed/.test(type)) state.running = false;
|
|
135
163
|
if (type === 'error' || event.error) state.failed = true;
|
|
136
164
|
const usage = normalizeUsage(event);
|
|
@@ -194,10 +222,17 @@ function createGenericParser(dependencies) {
|
|
|
194
222
|
usages.push(usage);
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
|
-
[...messages.values()]
|
|
198
|
-
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) || a.order - b.order)
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
const orderedMessages = [...messages.values()]
|
|
226
|
+
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) || a.order - b.order);
|
|
227
|
+
orderedMessages.forEach(message => addMessage(session, message));
|
|
228
|
+
const lastConversation = [...orderedMessages].reverse()
|
|
229
|
+
.find(message => message.role === 'assistant' || message.role === 'user');
|
|
230
|
+
return {
|
|
231
|
+
firstUser,
|
|
232
|
+
usages,
|
|
233
|
+
lastConversationRole: lastConversation && lastConversation.role || '',
|
|
234
|
+
lastAssistantText: lastConversation && lastConversation.role === 'assistant' ? lastConversation.text : '',
|
|
235
|
+
};
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
function finalizeSession(session, provider, root, eventState, messageState, fileInfo) {
|
|
@@ -208,15 +243,23 @@ function createGenericParser(dependencies) {
|
|
|
208
243
|
const context = modelContextWindow(provider, session.model, root.context_window || root.contextWindow);
|
|
209
244
|
session.context = contextInfo(session.turnUsage.total || session.usage.total, context);
|
|
210
245
|
const age = Date.now() - fileInfo.mtimeMs;
|
|
246
|
+
const pendingUserInput = eventState.pendingUserInputCalls.size > 0;
|
|
247
|
+
const conversationalInput = messageState.lastConversationRole === 'assistant'
|
|
248
|
+
&& assistantRequestsUserResponse(messageState.lastAssistantText);
|
|
211
249
|
session.status = eventState.failed
|
|
212
250
|
? 'failed'
|
|
213
|
-
: ((eventState.running &&
|
|
251
|
+
: (pendingUserInput || (!eventState.running && conversationalInput)
|
|
252
|
+
? 'waiting'
|
|
253
|
+
: ((eventState.running && age < STALE_TURN_THRESHOLD_MS) || age < ACTIVE_THRESHOLD_MS
|
|
214
254
|
? 'running'
|
|
215
|
-
: 'idle');
|
|
255
|
+
: 'idle'));
|
|
216
256
|
session.statusDetail = eventState.failed
|
|
217
257
|
? '오류 발생'
|
|
218
|
-
: (session.status === '
|
|
219
|
-
|
|
258
|
+
: (session.status === 'waiting'
|
|
259
|
+
? (pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기')
|
|
260
|
+
: (session.status === 'running' ? '실시간 이벤트 수신 중' : '다음 요청 대기'));
|
|
261
|
+
session.statusObserved = eventState.running || session.status === 'waiting';
|
|
262
|
+
session.executions = eventState.executionTracker.finalize();
|
|
220
263
|
trimSession(session);
|
|
221
264
|
return session;
|
|
222
265
|
}
|
|
@@ -18,6 +18,22 @@ function createHierarchyAttacher(dependencies) {
|
|
|
18
18
|
const parent = byId.get(session.parentId);
|
|
19
19
|
if (parent && !parent.childIds.includes(session.id)) parent.childIds.push(session.id);
|
|
20
20
|
}
|
|
21
|
+
const inheritOrigin = (session, seen = new Set()) => {
|
|
22
|
+
if (!session || !session.parentId || seen.has(session.id)) return;
|
|
23
|
+
seen.add(session.id);
|
|
24
|
+
const parent = byId.get(session.parentId);
|
|
25
|
+
if (!parent) return;
|
|
26
|
+
inheritOrigin(parent, seen);
|
|
27
|
+
const hadOrigin = Boolean(session.originCwd || session.cwd);
|
|
28
|
+
if (!session.cwd) session.cwd = parent.cwd;
|
|
29
|
+
if (!session.originCwd) session.originCwd = parent.originCwd || parent.cwd;
|
|
30
|
+
if (!hadOrigin) {
|
|
31
|
+
session.workspace = parent.workspace;
|
|
32
|
+
session.projectless = parent.projectless;
|
|
33
|
+
}
|
|
34
|
+
if (!session.environment) session.environment = parent.environment;
|
|
35
|
+
};
|
|
36
|
+
for (const session of sessions) inheritOrigin(session);
|
|
21
37
|
return byId;
|
|
22
38
|
}
|
|
23
39
|
|
|
@@ -57,6 +73,7 @@ function createHierarchyAttacher(dependencies) {
|
|
|
57
73
|
child.clientKind = parent.clientKind;
|
|
58
74
|
child.model = parent.model;
|
|
59
75
|
child.cwd = parent.cwd;
|
|
76
|
+
child.originCwd = parent.originCwd || parent.cwd;
|
|
60
77
|
child.workspace = parent.workspace;
|
|
61
78
|
child.projectless = parent.projectless;
|
|
62
79
|
child.environment = parent.environment;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const USER_INPUT_TOOL_PATTERN = /^(?:request_user_input|ask_user_question|askuserquestion|request_input|get_user_input)$/i;
|
|
4
|
+
|
|
5
|
+
function isUserInputTool(name) {
|
|
6
|
+
return USER_INPUT_TOOL_PATTERN.test(String(name || '').trim());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function conversationalTail(value) {
|
|
10
|
+
const text = String(value || '')
|
|
11
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
12
|
+
.replace(/~~~[\s\S]*?~~~/g, ' ')
|
|
13
|
+
.replace(/`[^`\r\n]*`/g, ' ')
|
|
14
|
+
.replace(/^\s*>.*$/gm, ' ')
|
|
15
|
+
.replace(/!?(?:\[[^\]]*\])\([^\s)]+(?:\s+"[^"]*")?\)/g, '$1')
|
|
16
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
17
|
+
.replace(/[ \t]+/g, ' ')
|
|
18
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
19
|
+
.trim();
|
|
20
|
+
return text.slice(-1600).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assistantRequestsUserResponse(value) {
|
|
24
|
+
const tail = conversationalTail(value);
|
|
25
|
+
if (!tail) return false;
|
|
26
|
+
|
|
27
|
+
// A question at the end of the final prose is the strongest provider-neutral signal.
|
|
28
|
+
if (/[??]\s*(?:["')\]}>*_~]|>)*\s*$/.test(tail)) return true;
|
|
29
|
+
|
|
30
|
+
const koreanRequest = /(?:선택|골라|알려|말씀|답변|확인|결정|지정|입력|보내|첨부|업로드|제공)(?:해|하여|해서|해\s*)?(?:주세요|주십시오|주실래요|바랍니다)(?:[.!::]|\s|$)/.test(tail);
|
|
31
|
+
const englishRequest = [
|
|
32
|
+
// A polite request may follow introductory prose, so "please"/"kindly"
|
|
33
|
+
// is a strong signal wherever it appears.
|
|
34
|
+
/\b(?:please|kindly)\s+(?:choose|select|confirm|tell\s+me|let\s+me\s+know|provide|send|share|enter|upload|attach)\b/i,
|
|
35
|
+
// Bare imperatives are requests only at the beginning of prose or a new
|
|
36
|
+
// sentence. This avoids treating technical words such as "resend" or a
|
|
37
|
+
// Korean completion report containing "attach" as user-input requests.
|
|
38
|
+
/(?:^|[\r\n]+|[.!?]\s+)\s*(?:choose|select|confirm|tell\s+me|let\s+me\s+know|provide|send|share|enter|upload|attach)\b/i,
|
|
39
|
+
/(?:^|[\r\n]+|[.!?]\s+)\s*(?:can|could|would|will)\s+you\s+(?:choose|select|confirm|provide|send|share|enter|upload|attach)\b/i,
|
|
40
|
+
].some(pattern => pattern.test(tail));
|
|
41
|
+
const chineseRequest = /(?:请选择|请确认|请提供|请告诉|请回复|请上传)/.test(tail);
|
|
42
|
+
const directRequest = koreanRequest || englishRequest || chineseRequest;
|
|
43
|
+
if (!directRequest) return false;
|
|
44
|
+
|
|
45
|
+
// Ignore common courtesy offers that do not block the task.
|
|
46
|
+
if (/(?:궁금한|추가 질문|도움이 필요).{0,30}(?:알려|말씀).{0,12}주세요[.!]?$/i.test(tail)) return false;
|
|
47
|
+
if (/let me know if (?:you have|there are) (?:any )?(?:questions|issues)[.!]?$/i.test(tail)) return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { assistantRequestsUserResponse, isUserInputTool };
|
package/src/agentMonitor.js
CHANGED
|
@@ -14,6 +14,7 @@ const { createCodexParser } = require('./agentMonitor/codexParser');
|
|
|
14
14
|
const { createClaudeParser } = require('./agentMonitor/claudeParser');
|
|
15
15
|
const { createGenericParser } = require('./agentMonitor/genericParser');
|
|
16
16
|
const { createHierarchyAttacher } = require('./agentMonitor/hierarchy');
|
|
17
|
+
const { assistantRequestsUserResponse, isUserInputTool } = require('./agentMonitor/responseIntent');
|
|
17
18
|
const {
|
|
18
19
|
MAX_FILES_PER_PROVIDER,
|
|
19
20
|
readJson,
|
|
@@ -178,10 +179,12 @@ function baseSession(provider, externalId, file, stat) {
|
|
|
178
179
|
title: '제목을 불러오는 중',
|
|
179
180
|
model: '',
|
|
180
181
|
cwd: '',
|
|
182
|
+
originCwd: '',
|
|
181
183
|
branch: '',
|
|
182
184
|
source: 'local-history',
|
|
183
185
|
sourceLabel: '로컬 세션',
|
|
184
186
|
clientKind: '',
|
|
187
|
+
utilityKind: '',
|
|
185
188
|
status: 'idle',
|
|
186
189
|
statusDetail: '',
|
|
187
190
|
statusObserved: false,
|
|
@@ -198,6 +201,7 @@ function baseSession(provider, externalId, file, stat) {
|
|
|
198
201
|
context: { used: 0, window: 0, percent: 0, source: 'unknown' },
|
|
199
202
|
messages: [],
|
|
200
203
|
lifecycle: [],
|
|
204
|
+
executions: [],
|
|
201
205
|
childIds: [],
|
|
202
206
|
collaboration: {
|
|
203
207
|
capacity: { totalThreads: 0, subagents: 0, source: 'unknown' },
|
|
@@ -207,6 +211,7 @@ function baseSession(provider, externalId, file, stat) {
|
|
|
207
211
|
retainedObserved: false,
|
|
208
212
|
metrics: null,
|
|
209
213
|
},
|
|
214
|
+
loop: null,
|
|
210
215
|
};
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -234,6 +239,8 @@ const parseClaude = createClaudeParser({
|
|
|
234
239
|
sumUsage,
|
|
235
240
|
timestamp,
|
|
236
241
|
trimSession,
|
|
242
|
+
assistantRequestsUserResponse,
|
|
243
|
+
isUserInputTool,
|
|
237
244
|
});
|
|
238
245
|
|
|
239
246
|
function codexUsage(raw = {}) {
|
|
@@ -258,6 +265,7 @@ function codexVisibleUserText(value) {
|
|
|
258
265
|
if (objective) return compactText(objective[1], 6000);
|
|
259
266
|
const desktopRequest = raw.match(/##\s*My request for Codex:\s*([\s\S]*?)(?:\n{2,}<image\b|$)/i);
|
|
260
267
|
if (desktopRequest) return compactText(desktopRequest[1], 6000);
|
|
268
|
+
if (/^<(?:subagent_notification|task_notification|task-notification|agent_notification|collaboration_notification)(?:\s|>)/i.test(raw)) return '';
|
|
261
269
|
if (/^<(?:permissions instructions|app-context|environment_context|skills_instructions|plugins_instructions|apps_instructions|multi_agent_mode|collaboration_mode)>/i.test(raw)) return '';
|
|
262
270
|
if (/^<skill(?:\s|>)/i.test(raw)) return '';
|
|
263
271
|
if (/^#\s*Codex desktop context/i.test(raw)) return '';
|
|
@@ -302,6 +310,7 @@ const parseCodex = createCodexParser({
|
|
|
302
310
|
textOps: {
|
|
303
311
|
agentEnvelope, codexContentText, codexVisibleUserText,
|
|
304
312
|
compactText, encryptedCollaborationText, jsonObject,
|
|
313
|
+
assistantRequestsUserResponse, isUserInputTool,
|
|
305
314
|
},
|
|
306
315
|
collaborationOps: {
|
|
307
316
|
collaborationCapacity, collaborationTaskName,
|
|
@@ -328,6 +337,8 @@ const parseGeneric = createGenericParser({
|
|
|
328
337
|
sumUsage,
|
|
329
338
|
timestamp,
|
|
330
339
|
trimSession,
|
|
340
|
+
assistantRequestsUserResponse,
|
|
341
|
+
isUserInputTool,
|
|
331
342
|
});
|
|
332
343
|
|
|
333
344
|
function contextInfo(used, windowInfo) {
|
|
@@ -363,6 +374,7 @@ function parseManagedSession(runDir) {
|
|
|
363
374
|
sourceLabel: 'LoadToAgent 실행',
|
|
364
375
|
statusObserved: true,
|
|
365
376
|
};
|
|
377
|
+
session.originCwd = session.originCwd || meta.cwd || session.cwd || '';
|
|
366
378
|
session.usage = finalizeUsage(session.usage);
|
|
367
379
|
session.turnUsage = finalizeUsage(session.turnUsage);
|
|
368
380
|
const window = modelContextWindow(session.provider, session.model, session.context && session.context.window);
|
|
@@ -378,8 +390,9 @@ function workspaceLabel(cwd) {
|
|
|
378
390
|
}
|
|
379
391
|
|
|
380
392
|
function isProjectlessSession(session) {
|
|
381
|
-
|
|
382
|
-
|
|
393
|
+
const cwd = session && (session.originCwd || session.cwd);
|
|
394
|
+
if (!cwd) return true;
|
|
395
|
+
const normalized = String(cwd).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
383
396
|
return session.provider === 'codex'
|
|
384
397
|
&& session.clientKind === 'codex-desktop'
|
|
385
398
|
&& /(?:^|\/)Documents\/Codex\/\d{4}-\d{2}-\d{2}\/new-chat$/i.test(normalized);
|
|
@@ -528,6 +541,10 @@ class AgentMonitor extends EventEmitter {
|
|
|
528
541
|
const value = this.parseFile(info, parser);
|
|
529
542
|
if (!value) continue;
|
|
530
543
|
const copy = structuredClone(value);
|
|
544
|
+
// Provider health checks and detached memory extraction turns are
|
|
545
|
+
// implementation details, not user work. Keep them out of the
|
|
546
|
+
// dashboard and runtime-link candidate pool entirely.
|
|
547
|
+
if (copy.utilityKind) continue;
|
|
531
548
|
copy.environment = { kind: history.kind, distro: history.distro, label: history.label, home: history.home };
|
|
532
549
|
if (history.kind === 'wsl') copy.sourceLabel = history.label;
|
|
533
550
|
sessions.push(copy);
|
|
@@ -548,8 +565,9 @@ class AgentMonitor extends EventEmitter {
|
|
|
548
565
|
for (const session of managed) byId.set(session.id, session);
|
|
549
566
|
const merged = [...byId.values()]
|
|
550
567
|
.map(session => {
|
|
568
|
+
const originCwd = session.originCwd || session.cwd || '';
|
|
551
569
|
const projectless = isProjectlessSession(session);
|
|
552
|
-
return { ...session, projectless, workspace: projectless ? '프로젝트 없음' : workspaceLabel(
|
|
570
|
+
return { ...session, originCwd, projectless, workspace: projectless ? '프로젝트 없음' : workspaceLabel(originCwd) };
|
|
553
571
|
})
|
|
554
572
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
555
573
|
attachHierarchy(merged);
|
package/src/agentRunner.js
CHANGED
|
@@ -385,7 +385,7 @@ class AgentRunner extends EventEmitter {
|
|
|
385
385
|
if (run.stopping) {
|
|
386
386
|
state.status = 'cancelled';
|
|
387
387
|
state.statusDetail = '사용자가 중지함';
|
|
388
|
-
} else if (state.status === 'running' || state.status === 'starting') {
|
|
388
|
+
} else if (state.status === 'running' || state.status === 'starting' || state.status === 'paused') {
|
|
389
389
|
state.status = code === 0 ? 'completed' : 'failed';
|
|
390
390
|
state.statusDetail = code === 0 ? '작업 완료' : `CLI 종료 코드 ${code}${signal ? ` · ${signal}` : ''}`;
|
|
391
391
|
}
|
|
@@ -450,10 +450,75 @@ class AgentRunner extends EventEmitter {
|
|
|
450
450
|
if (process.platform === 'win32') {
|
|
451
451
|
execFile('taskkill', ['/PID', String(run.child.pid), '/T', '/F'], { windowsHide: true }, () => {});
|
|
452
452
|
} else {
|
|
453
|
+
if (run.state.status === 'paused') runBestEffort('runner-stop-resume', () => process.kill(run.child.pid, 'SIGCONT'));
|
|
453
454
|
runBestEffort('runner-stop', () => run.child.kill('SIGTERM'));
|
|
454
455
|
}
|
|
455
456
|
return { ok: true };
|
|
456
457
|
}
|
|
458
|
+
|
|
459
|
+
retry(id) {
|
|
460
|
+
const runIdValue = String(id || '');
|
|
461
|
+
if (!/^[a-z0-9-]{4,120}$/i.test(runIdValue)) return { ok: false, error: '다시 실행할 작업 ID가 올바르지 않습니다.' };
|
|
462
|
+
if (this.active.has(runIdValue)) return { ok: false, error: '아직 실행 중인 작업은 다시 실행할 수 없습니다.' };
|
|
463
|
+
const file = path.join(this.runsDir, runIdValue, 'meta.json');
|
|
464
|
+
let meta;
|
|
465
|
+
try {
|
|
466
|
+
meta = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
467
|
+
} catch (_unavailableRunMetadata) {
|
|
468
|
+
return { ok: false, error: '이전 실행 설정을 찾을 수 없습니다.' };
|
|
469
|
+
}
|
|
470
|
+
const result = this.start({
|
|
471
|
+
provider: meta.provider,
|
|
472
|
+
prompt: meta.prompt,
|
|
473
|
+
cwd: meta.cwd,
|
|
474
|
+
model: meta.model,
|
|
475
|
+
allowWrites: Boolean(meta.allowWrites),
|
|
476
|
+
});
|
|
477
|
+
return result && result.ok ? { ...result, retriedFrom: runIdValue } : result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
setPaused(id, paused) {
|
|
481
|
+
const run = this.active.get(String(id || ''));
|
|
482
|
+
if (!run) return Promise.resolve({ ok: false, error: '실행 중인 작업을 찾을 수 없습니다.' });
|
|
483
|
+
if (paused && run.state.status === 'paused') return Promise.resolve({ ok: true, status: 'paused' });
|
|
484
|
+
if (!paused && run.state.status !== 'paused') return Promise.resolve({ ok: true, status: run.state.status });
|
|
485
|
+
const applyState = () => {
|
|
486
|
+
run.state.status = paused ? 'paused' : 'running';
|
|
487
|
+
run.state.statusDetail = paused ? '사용자가 실행을 일시정지함' : '사용자가 실행을 다시 시작함';
|
|
488
|
+
addLifecycle(run.state, paused ? 'process-pause' : 'process-resume', paused ? '실행 일시정지' : '실행 다시 시작', {
|
|
489
|
+
id: `${paused ? 'pause' : 'resume'}:${Date.now()}`,
|
|
490
|
+
status: paused ? 'done' : 'running',
|
|
491
|
+
});
|
|
492
|
+
run.state.updatedAt = new Date().toISOString();
|
|
493
|
+
this.persist(run);
|
|
494
|
+
this.emit('changed', { runId: run.id, state: run.state });
|
|
495
|
+
return { ok: true, status: run.state.status };
|
|
496
|
+
};
|
|
497
|
+
if (process.platform !== 'win32') {
|
|
498
|
+
try {
|
|
499
|
+
process.kill(run.child.pid, paused ? 'SIGSTOP' : 'SIGCONT');
|
|
500
|
+
return Promise.resolve(applyState());
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return Promise.resolve({ ok: false, error: error.message });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
506
|
+
const powershell = path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
507
|
+
const command = `${paused ? 'Suspend' : 'Resume'}-Process -Id ${Number(run.child.pid)} -ErrorAction Stop`;
|
|
508
|
+
return new Promise(resolve => {
|
|
509
|
+
execFile(powershell, ['-NoProfile', '-NonInteractive', '-Command', command], { windowsHide: true }, error => {
|
|
510
|
+
resolve(error ? { ok: false, error: error.message } : applyState());
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
pause(id) {
|
|
516
|
+
return this.setPaused(id, true);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
resume(id) {
|
|
520
|
+
return this.setPaused(id, false);
|
|
521
|
+
}
|
|
457
522
|
}
|
|
458
523
|
|
|
459
524
|
module.exports = {
|
package/src/attentionNotifier.js
CHANGED
|
@@ -7,24 +7,28 @@ class AttentionNotifier {
|
|
|
7
7
|
this.copy = options.copy || (() => ({ title: '내 확인이 필요합니다', body: '응답이나 선택을 기다리는 AI 세션이 있습니다.' }));
|
|
8
8
|
this.onOpen = options.onOpen || (() => {});
|
|
9
9
|
this.onFallback = options.onFallback || (() => {});
|
|
10
|
-
this.
|
|
10
|
+
this.attentionIds = null;
|
|
11
11
|
this.notifications = new Set();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
sync(snapshot) {
|
|
15
|
-
const
|
|
16
|
-
.filter(session => session && session.id &&
|
|
17
|
-
|
|
15
|
+
const needsAttention = (snapshot && Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
|
|
16
|
+
.filter(session => session && session.id && (
|
|
17
|
+
session.attention?.required
|
|
18
|
+
|| session.health?.level === 'critical'
|
|
19
|
+
|| session.status === 'waiting'
|
|
20
|
+
));
|
|
21
|
+
const nextIds = new Set(needsAttention.map(session => String(session.id)));
|
|
18
22
|
|
|
19
|
-
if (this.
|
|
20
|
-
this.
|
|
23
|
+
if (this.attentionIds === null) {
|
|
24
|
+
this.attentionIds = nextIds;
|
|
21
25
|
return [];
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
+
const newlyActionable = needsAttention.filter(session => !this.attentionIds.has(String(session.id)));
|
|
29
|
+
this.attentionIds = nextIds;
|
|
30
|
+
newlyActionable.forEach(session => this.notify(session));
|
|
31
|
+
return newlyActionable.map(session => String(session.id));
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
notify(session) {
|