loadtoagent 1.3.1 → 1.3.2
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 +11 -9
- package/docs/PROVIDER-CONTRACTS.md +8 -6
- package/main.js +25 -3
- package/package.json +1 -1
- package/preload.js +7 -2
- package/renderer/app-dashboard.js +18 -12
- package/renderer/app-graph-view.js +1 -1
- package/renderer/app-management.js +91 -45
- package/renderer/app-run-modal.js +1 -1
- package/renderer/app.js +1 -1
- package/renderer/i18n-messages.js +212 -199
- package/renderer/index.html +28 -27
- package/renderer/styles-agent-map.css +10 -10
- package/renderer/styles-cards.css +19 -19
- package/renderer/styles-collaboration.css +23 -23
- package/renderer/styles-components.css +36 -36
- package/renderer/styles-management.css +23 -14
- package/renderer/styles-onboarding.css +8 -8
- package/renderer/styles-overlays.css +5 -5
- package/renderer/styles-product.css +24 -24
- package/renderer/styles-quality.css +4 -4
- package/renderer/styles-readability.css +179 -1
- package/renderer/styles-responsive-runtime.css +5 -5
- package/renderer/styles-responsive-workflows.css +2 -2
- package/renderer/styles-run-composer.css +23 -23
- package/renderer/styles-runtime-overview.css +22 -22
- package/renderer/styles-settings.css +21 -21
- package/renderer/styles-terminal.css +66 -66
- package/renderer/styles-tmux.css +49 -49
- package/renderer/styles-workflows.css +48 -48
- package/renderer/styles.css +7 -7
- package/renderer/terminal-events.js +34 -11
- package/renderer/terminal-workbench.js +52 -21
- package/renderer/terminal.js +9 -1
- package/src/contracts.js +5 -5
- package/src/ipc/registerTerminalIpc.js +6 -14
- package/src/sessionIntelligence.js +18 -4
- package/src/terminalHost.js +98 -26
- package/src/terminalHostDaemon.js +1 -0
- package/src/terminalManager.js +61 -4
package/README.ko.md
CHANGED
|
@@ -70,14 +70,14 @@ LoadToAgent는 시작할 때 현재 패키지 버전과 GitHub의 최신 정식
|
|
|
70
70
|
- macOS 또는 Windows
|
|
71
71
|
- npm으로 설치할 때만 Node.js 18 이상
|
|
72
72
|
- Claude Code, Codex CLI, Gemini CLI, Grok CLI 중 하나 이상 설치 및 로그인
|
|
73
|
-
- tmux
|
|
73
|
+
- tmux 터미널 관리를 사용할 때만 tmux 필요
|
|
74
74
|
|
|
75
75
|
## 처음 10분 사용법
|
|
76
76
|
|
|
77
77
|
1. **홈**에서 `새 AI 작업`을 누르고 할 일과 작업 폴더를 고릅니다. 설치된 AI가 없으면 화면의 공식 설치 안내를 먼저 따르세요.
|
|
78
78
|
2. **진행 중**에서 초록 상태의 AI를 확인합니다. 도움 AI를 함께 쓰는 작업은 접힌 `상세 흐름 보기`에서 펼칠 수 있습니다.
|
|
79
|
-
3.
|
|
80
|
-
4. 작업 카드를 열어 **대화·진행 과정·사용량**을 확인하고, 연결된 작업은
|
|
79
|
+
3. **확인할 일**에 숫자가 생기면 내 응답이 필요한 작업과 상태 위험 신호를 구분해 확인합니다.
|
|
80
|
+
4. 작업 카드를 열어 **대화·진행 과정·사용량**을 확인하고, 연결된 작업은 **AI 세션 터미널**에서 이어서 입력합니다.
|
|
81
81
|
|
|
82
82
|
홈의 `10분 시작 가이드`도 같은 네 단계를 직접 눌러 연습하게 해 줍니다. 완료 상태는 이 컴퓨터에 저장되며 언제든 다시 펼칠 수 있습니다.
|
|
83
83
|
|
|
@@ -87,18 +87,20 @@ LoadToAgent는 시작할 때 현재 패키지 버전과 GitHub의 최신 정식
|
|
|
87
87
|
|---|---|
|
|
88
88
|
| AI 작업 지도 | Claude, Codex, Gemini, Grok별 실시간 작업 |
|
|
89
89
|
| 연결 관계 | 사용자 요청, 선택한 메인 AI, 직접 나눠 맡긴 도움 AI |
|
|
90
|
-
| 실행
|
|
91
|
-
|
|
|
90
|
+
| 명령·백그라운드 실행 | AI가 시작한 일반 명령과 백그라운드 명령의 내용, 작업 폴더, 실행 ID와 현재 상태 |
|
|
91
|
+
| 응답·상태 확인함 | 최근 24시간 세션에서 내 응답이 필요한 요청과 현재 실행의 실패·지연·일시정지 위험만 확인 |
|
|
92
92
|
| 작업 상세 | 대화, 도구 호출, 진행 과정, 모델, 작업 폴더, 상태 |
|
|
93
|
-
| 관리 요약 |
|
|
93
|
+
| 관리 요약 | 최근 실행 이벤트, 상태 정보 확인 수준, 완료 신호, 로그에서 찾은 산출물 후보와 테스트 기록, 실행 제어 |
|
|
94
94
|
| 토큰 | 입력·출력·캐시·추론·전체 사용량과 보고된 컨텍스트 점유율 |
|
|
95
|
-
| 세션 터미널 | 선택한 AI의 이전 대화와 기존 PTY·tmux 화면을 나란히 보며 같은 세션에 이어서 입력 |
|
|
96
|
-
| tmux
|
|
95
|
+
| AI 세션 터미널 | 선택한 AI의 이전 대화와 기존 PTY·tmux 화면을 나란히 보며 같은 세션에 이어서 입력 |
|
|
96
|
+
| tmux 터미널 관리 | macOS 또는 Windows WSL의 tmux 세션 → 창 → 패널 → AI 프로세스 관계 |
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
예약·실행 단계, AI 세션 터미널, tmux 터미널 관리는 왼쪽의 **고급 도구** 아래에 모여 있어 일상적인 관제 흐름을 방해하지 않습니다.
|
|
99
99
|
|
|
100
100
|
LoadToAgent는 `직접 입력 가능`, `브리지 연결 후 입력 가능`, `원래 앱에서 계속해야 하는 보기 전용`, `종료된 세션`을 구분합니다. 임의의 외부 창에 키 입력을 보내지 않습니다.
|
|
101
101
|
|
|
102
|
+
홈과 세션 목록은 현재 실행 중이거나 마지막 활동이 24시간 이내인 세션을 표시합니다. **확인할 일**에는 이 최근 세션 중 실제 응답 요청 또는 현재 실행의 실패·지연·일시정지 위험 신호만 포함하며, 정보 확인 수준이 낮다는 이유만으로는 포함하지 않습니다.
|
|
103
|
+
|
|
102
104
|
## 연결된 터미널 사용
|
|
103
105
|
|
|
104
106
|
LoadToAgent 앱을 열어 둔 뒤 인증된 로컬 브리지로 AI CLI를 시작합니다.
|
|
@@ -11,17 +11,19 @@ LoadToAgent는 제공사별 이벤트를 아래 공통 단계로 정규화합니
|
|
|
11
11
|
제공사별 원본 세션은 화면에 전달되기 전에 다음 관리 정보로 보강됩니다.
|
|
12
12
|
|
|
13
13
|
- `attention`: 승인·결정·입력·오류·일시정지처럼 사용자 조치가 필요한 이유와 관측 신뢰도
|
|
14
|
-
- `progress`:
|
|
15
|
-
- `health`: 실패,
|
|
14
|
+
- `progress`: 최근 실행 이벤트 중 완료로 기록된 수, 최근 체크포인트, 막힘 사유와 마지막 활동 시각. 화면의 비율은 전체 계획 진척도가 아닙니다.
|
|
15
|
+
- `health`: 실패, 새 활동 없음, 장시간 응답 대기, AI 기억 한도, 반복 실패, 부모 연결 정보 누락 신호
|
|
16
16
|
- `controlCapabilities`: 실제 연결과 관리 실행 상태에 근거한 응답·중지·일시정지·재개·재시도·재배정 가능 여부
|
|
17
|
-
- `evidence`:
|
|
18
|
-
- `outcome`: 완료 요약,
|
|
17
|
+
- `evidence`: 상태·부모/도움 AI 관계·완료 신호가 직접 확인인지 활동 기록 추정인지와 정보 출처. 확인 수준은 결과 품질의 신뢰도가 아닙니다.
|
|
18
|
+
- `outcome`: 완료 요약, 로그에서 찾은 파일·테스트·커밋 후보, 테스트 관련 로그와 완료 신호 수신 여부. 파일 존재나 결과 정확성을 독립 검증한 값이 아닙니다.
|
|
19
19
|
|
|
20
20
|
관측 이벤트가 없는 값은 사실로 단정하지 않고 `inferred` 또는 `unverified`로 표시합니다. 시간 기반 건강 신호는 매분 다시 계산됩니다.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
화면의 최근 세션 기준은 마지막 활동 후 24시간입니다. 실행 중인 세션은 마지막 활동 시각과 관계없이 계속 표시합니다. `확인할 일`은 이 범위 안의 실제 응답 요청과 현재 실행 위험만 집계하며, 오래된 세션이나 단순한 낮은 확인 수준은 제외합니다.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## 명령·백그라운드 실행 계약
|
|
25
|
+
|
|
26
|
+
- 각 AI 세션은 `executions`에 자신이 시작한 일반 명령과 백그라운드 명령을 별도 기록으로 보관합니다. 도움 AI 세션과 섞어 세지 않습니다.
|
|
25
27
|
- 공통 필드는 `kind(shell|background)`, `mode(foreground|background)`, `status(running|completed|failed)`, 명령, 작업 폴더, 호출 ID, 시작·갱신·완료 시각입니다.
|
|
26
28
|
- Codex는 `shell_command`, `exec_command`와 `custom_tool_call: exec` 안의 `tools.exec_command(...)`를 인식합니다. 장기 실행이 반환한 cell/session ID는 후속 `wait`·`write_stdin` 호출과 연결합니다.
|
|
27
29
|
- Claude는 `Bash` 계열의 `run_in_background`와 후속 `TaskOutput`·`BashOutput`을 같은 실행으로 연결합니다. Gemini/Grok의 구조화 `tool_use`·`tool_result`에서도 셸 도구를 같은 계약으로 정규화합니다.
|
package/main.js
CHANGED
|
@@ -10,7 +10,7 @@ const { execFileSync } = require('child_process');
|
|
|
10
10
|
const { AgentRunner, probeProviders } = require('./src/agentRunner');
|
|
11
11
|
const { providerList, blankUsage } = require('./src/providerRegistry');
|
|
12
12
|
const { TerminalManager } = require('./src/terminalManager');
|
|
13
|
-
const { TerminalHostClient, launchTerminalHost } = require('./src/terminalHost');
|
|
13
|
+
const { TerminalHostClient, launchTerminalHost, resolveTerminalHostExecutable } = require('./src/terminalHost');
|
|
14
14
|
const { TmuxController } = require('./src/tmuxController');
|
|
15
15
|
const { normalizeWslList } = require('./src/tmuxMonitor');
|
|
16
16
|
const { UpdateManager } = require('./src/updateManager');
|
|
@@ -59,6 +59,9 @@ const MAIN_COPY = {
|
|
|
59
59
|
pickWorkspace: '작업 폴더 선택',
|
|
60
60
|
attentionTitle: '내 확인이 필요합니다',
|
|
61
61
|
attentionBody: '{provider} · {title}',
|
|
62
|
+
terminalHostReconnecting: '터미널 연결을 자동으로 복구하는 중입니다.',
|
|
63
|
+
terminalHostReconnected: '터미널 연결을 복구했습니다.',
|
|
64
|
+
terminalHostReconnectFailed: '터미널 연결을 복구하지 못했습니다: {reason}',
|
|
62
65
|
},
|
|
63
66
|
en: {
|
|
64
67
|
trayTooltip: 'LoadToAgent · {count} background terminals',
|
|
@@ -69,6 +72,9 @@ const MAIN_COPY = {
|
|
|
69
72
|
pickWorkspace: 'Choose workspace',
|
|
70
73
|
attentionTitle: 'Your review is needed',
|
|
71
74
|
attentionBody: '{provider} · {title}',
|
|
75
|
+
terminalHostReconnecting: 'Restoring the terminal connection automatically.',
|
|
76
|
+
terminalHostReconnected: 'Terminal connection restored.',
|
|
77
|
+
terminalHostReconnectFailed: 'Could not restore the terminal connection: {reason}',
|
|
72
78
|
},
|
|
73
79
|
'zh-CN': {
|
|
74
80
|
trayTooltip: 'LoadToAgent · {count} 个后台终端',
|
|
@@ -79,6 +85,9 @@ const MAIN_COPY = {
|
|
|
79
85
|
pickWorkspace: '选择工作文件夹',
|
|
80
86
|
attentionTitle: '需要你的确认',
|
|
81
87
|
attentionBody: '{provider} · {title}',
|
|
88
|
+
terminalHostReconnecting: '正在自动恢复终端连接。',
|
|
89
|
+
terminalHostReconnected: '终端连接已恢复。',
|
|
90
|
+
terminalHostReconnectFailed: '无法恢复终端连接:{reason}',
|
|
82
91
|
},
|
|
83
92
|
};
|
|
84
93
|
let lastSnapshot = {
|
|
@@ -388,7 +397,7 @@ async function setupRuntime() {
|
|
|
388
397
|
: new TerminalHostClient({
|
|
389
398
|
discoveryFile: terminalHostFile,
|
|
390
399
|
spawnHost: () => launchTerminalHost({
|
|
391
|
-
executable:
|
|
400
|
+
executable: resolveTerminalHostExecutable({ isPackaged: app.isPackaged }),
|
|
392
401
|
script: path.join(__dirname, 'src', 'terminalHostDaemon.js'),
|
|
393
402
|
storeFile: terminalStoreFile,
|
|
394
403
|
discoveryFile: terminalHostFile,
|
|
@@ -426,7 +435,20 @@ async function setupRuntime() {
|
|
|
426
435
|
if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
|
|
427
436
|
});
|
|
428
437
|
terminalManager.on('disconnect', () => {
|
|
429
|
-
sendTerminal('terminals:
|
|
438
|
+
sendTerminal('terminals:connection', { state: 'reconnecting', message: mainText('terminalHostReconnecting') });
|
|
439
|
+
});
|
|
440
|
+
terminalManager.on('reconnect', payload => {
|
|
441
|
+
const sessions = visibleTerminalSessions(payload?.sessions || terminalManager.list());
|
|
442
|
+
sendTerminal('terminals:state', { change: 'reconnected', session: null, sessions });
|
|
443
|
+
sendTerminal('terminals:connection', { state: 'connected', message: mainText('terminalHostReconnected') });
|
|
444
|
+
updateBackgroundTrayMenu();
|
|
445
|
+
if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
|
|
446
|
+
});
|
|
447
|
+
terminalManager.on('reconnect-error', error => {
|
|
448
|
+
sendTerminal('terminals:connection', {
|
|
449
|
+
state: 'failed',
|
|
450
|
+
message: mainText('terminalHostReconnectFailed', { reason: error?.message || String(error) }),
|
|
451
|
+
});
|
|
430
452
|
});
|
|
431
453
|
await terminalManager.connect();
|
|
432
454
|
availability = probeProviders();
|
package/package.json
CHANGED
package/preload.js
CHANGED
|
@@ -35,9 +35,9 @@ contextBridge.exposeInMainWorld('loadtoagent', {
|
|
|
35
35
|
wslDistros: () => ipcRenderer.invoke('wsl:list-distros'),
|
|
36
36
|
terminalGet: id => ipcRenderer.invoke('terminals:get', id),
|
|
37
37
|
terminalCreate: options => ipcRenderer.invoke('terminals:create', options),
|
|
38
|
-
terminalWrite: (id, data) => ipcRenderer.
|
|
38
|
+
terminalWrite: (id, data) => ipcRenderer.invoke('terminals:write', id, data),
|
|
39
39
|
terminalCommand: (id, command) => ipcRenderer.invoke('terminals:command', id, command),
|
|
40
|
-
terminalResize: (id, cols, rows) => ipcRenderer.
|
|
40
|
+
terminalResize: (id, cols, rows) => ipcRenderer.invoke('terminals:resize', id, cols, rows),
|
|
41
41
|
terminalSignal: (id, signal) => ipcRenderer.invoke('terminals:signal', id, signal),
|
|
42
42
|
terminalRestart: id => ipcRenderer.invoke('terminals:restart', id),
|
|
43
43
|
terminalClose: id => ipcRenderer.invoke('terminals:close', id),
|
|
@@ -68,6 +68,11 @@ contextBridge.exposeInMainWorld('loadtoagent', {
|
|
|
68
68
|
ipcRenderer.on('terminals:error', handler);
|
|
69
69
|
return () => ipcRenderer.removeListener('terminals:error', handler);
|
|
70
70
|
},
|
|
71
|
+
onTerminalConnection: callback => {
|
|
72
|
+
const handler = (_event, payload) => callback(payload);
|
|
73
|
+
ipcRenderer.on('terminals:connection', handler);
|
|
74
|
+
return () => ipcRenderer.removeListener('terminals:connection', handler);
|
|
75
|
+
},
|
|
71
76
|
onSnapshot: callback => {
|
|
72
77
|
const handler = (_event, snapshot) => callback(snapshot);
|
|
73
78
|
ipcRenderer.on('agents:snapshot', handler);
|
|
@@ -18,6 +18,12 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
18
18
|
isRuntimeLoopSession = () => false,
|
|
19
19
|
} = context;
|
|
20
20
|
|
|
21
|
+
function displaySessions() {
|
|
22
|
+
return visibleSessions().filter((session) => (
|
|
23
|
+
typeof context.isRecentSession !== "function" || context.isRecentSession(session)
|
|
24
|
+
));
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
function renderProviderRail() {
|
|
22
28
|
$("#providerRail").innerHTML = visibleProviders()
|
|
23
29
|
.map((provider) => {
|
|
@@ -62,7 +68,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
62
68
|
const projects = new Map();
|
|
63
69
|
const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
|
|
64
70
|
saved.forEach((item) => projects.set(item.key, { path: item.path, name: item.name || projectName(item.path), saved: true, count: 0 }));
|
|
65
|
-
|
|
71
|
+
displaySessions().filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
|
|
66
72
|
const originPath = sessionOriginPath(session);
|
|
67
73
|
if (!originPath) return;
|
|
68
74
|
const owner = saved
|
|
@@ -104,7 +110,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
104
110
|
|
|
105
111
|
function renderWorkspaces() {
|
|
106
112
|
const lists = [$("#workspaceList"), $("#mobileWorkspaceList")].filter(Boolean);
|
|
107
|
-
const rootSessions =
|
|
113
|
+
const rootSessions = displaySessions().filter((session) => !session.parentId);
|
|
108
114
|
const projects = observedProjects();
|
|
109
115
|
const projectlessCount = rootSessions.filter(isProjectlessSession).length;
|
|
110
116
|
const savedWorkspaceExists = state.workspace === "all"
|
|
@@ -149,15 +155,15 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
function renderGlobalStats() {
|
|
152
|
-
const sessions =
|
|
158
|
+
const sessions = displaySessions();
|
|
153
159
|
const totals = {
|
|
154
160
|
active: sessions.filter((session) => session.status === "running" || session.status === "starting").length,
|
|
155
|
-
waiting: sessions.filter((session) => session
|
|
161
|
+
waiting: sessions.filter((session) => context.matchesManagementFilter?.(session, "attention")).length,
|
|
156
162
|
usage: { total: sessions.reduce((sum, session) => sum + Number(session.usage && session.usage.total || 0), 0) },
|
|
157
163
|
};
|
|
158
164
|
const rootCount = sessions.filter((session) => !session.parentId).length;
|
|
159
|
-
const criticalCount = sessions.filter((session) =>
|
|
160
|
-
const riskCount = sessions.filter((session) =>
|
|
165
|
+
const criticalCount = sessions.filter((session) => context.matchesManagementFilter?.(session, "critical")).length;
|
|
166
|
+
const riskCount = sessions.filter((session) => context.matchesManagementFilter?.(session, "warning")).length;
|
|
161
167
|
const items = [
|
|
162
168
|
[window.LoadToAgentI18n.t("ui.all_tasks"), rootCount, window.LoadToAgentI18n.t("ui.items"), ""],
|
|
163
169
|
[window.LoadToAgentI18n.t("ui.ai_working_now"), totals.active || 0, window.LoadToAgentI18n.t("ui.items"), "live"],
|
|
@@ -177,7 +183,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
177
183
|
$("#navAllCount").textContent = rootCount;
|
|
178
184
|
const activeRootCount = sessions.filter((session) => !session.parentId && ["running", "starting"].includes(session.status)).length;
|
|
179
185
|
$("#navActiveCount").textContent = activeRootCount;
|
|
180
|
-
const reviewCount = sessions.filter((session) => context.
|
|
186
|
+
const reviewCount = sessions.filter((session) => context.needsManagementReview?.(session)).length;
|
|
181
187
|
$("#navWaitingCount").textContent = reviewCount;
|
|
182
188
|
const scheduledCount = (state.snapshot?.automations || [])
|
|
183
189
|
.filter((item) => isProviderVisible(item.provider || "codex")).length;
|
|
@@ -201,7 +207,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
201
207
|
}[button.dataset.view];
|
|
202
208
|
const label = t(key);
|
|
203
209
|
const count = navCounts[button.dataset.view];
|
|
204
|
-
const unitKey = { all: "tasks", active: "tasks", waiting: "
|
|
210
|
+
const unitKey = { all: "tasks", active: "tasks", waiting: "items", runtime: "runs", terminal: "sessions", tmux: "sessions" }[button.dataset.view];
|
|
205
211
|
const unit = unitKey ? t(`quality.unit.${unitKey}`) : "";
|
|
206
212
|
const accessibleLabel = Number.isFinite(count) ? t("quality.nav_count_detailed", { label, count, unit }) : label;
|
|
207
213
|
button.setAttribute("aria-label", accessibleLabel);
|
|
@@ -341,7 +347,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
341
347
|
function renderProviderOverview() {
|
|
342
348
|
pruneProviderFilters();
|
|
343
349
|
const summaries = (state.snapshot && state.snapshot.summary && state.snapshot.summary.providers) || state.providers;
|
|
344
|
-
const sessions =
|
|
350
|
+
const sessions = displaySessions();
|
|
345
351
|
const visibleSummaries = summaries.filter((provider) => isProviderVisible(provider.id));
|
|
346
352
|
const overviewTabStopId = state.providerFilters.size ? [...state.providerFilters][0] : visibleSummaries[0]?.id;
|
|
347
353
|
$("#providerOverview").innerHTML = visibleSummaries
|
|
@@ -418,10 +424,10 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
418
424
|
}
|
|
419
425
|
|
|
420
426
|
function filteredSessions() {
|
|
421
|
-
const allSessions =
|
|
427
|
+
const allSessions = displaySessions();
|
|
422
428
|
let sessions = state.view === "waiting" ? allSessions : allSessions.filter((session) => !session.parentId);
|
|
423
429
|
if (state.view === "active") sessions = sessions.filter((session) => session.status === "running" || session.status === "starting");
|
|
424
|
-
if (state.view === "waiting") sessions = sessions.filter((session) =>
|
|
430
|
+
if (state.view === "waiting") sessions = sessions.filter((session) => context.needsManagementReview?.(session));
|
|
425
431
|
if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
|
|
426
432
|
sessions = sessions.filter(matchesWorkspaceFilter);
|
|
427
433
|
const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
|
|
@@ -445,7 +451,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
445
451
|
}
|
|
446
452
|
|
|
447
453
|
function graphFilteredSessions() {
|
|
448
|
-
let sessions =
|
|
454
|
+
let sessions = displaySessions();
|
|
449
455
|
if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
|
|
450
456
|
sessions = sessions.filter(matchesWorkspaceFilter);
|
|
451
457
|
const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
|
|
@@ -33,7 +33,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
33
33
|
} = context;
|
|
34
34
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
35
35
|
const statusLabel = (status) => ({
|
|
36
|
-
starting: t("ui.preparing"), running: t("ui.working"), waiting: t("
|
|
36
|
+
starting: t("ui.preparing"), running: t("ui.working"), waiting: t("ui.waiting_for_review"), idle: t("ui.idle"),
|
|
37
37
|
completed: t("ui.completed"), failed: t("ui.problem"), cancelled: t("ui.stopped"),
|
|
38
38
|
})[status] || STATUS[status] || status;
|
|
39
39
|
|
|
@@ -10,6 +10,33 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
10
10
|
const healthLabel = level => t(`management.health.${level || "unknown"}`);
|
|
11
11
|
const signalLabel = code => t(`management.signal.${code || "low-confidence"}`);
|
|
12
12
|
const evidenceLabel = value => t(`management.evidence.${value || "unverified"}`);
|
|
13
|
+
const RECENT_SESSION_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const ALWAYS_VISIBLE_STATUSES = new Set(["starting", "running"]);
|
|
15
|
+
const CURRENT_RISK_STATUSES = new Set(["starting", "running", "waiting", "paused", "failed"]);
|
|
16
|
+
const RESPONSE_ATTENTION_KINDS = new Set(["approval", "decision", "input", "response"]);
|
|
17
|
+
const needsUserResponse = session => Boolean(
|
|
18
|
+
session.attention?.required && RESPONSE_ATTENTION_KINDS.has(session.attention.kind),
|
|
19
|
+
);
|
|
20
|
+
const sessionActivityTimestamp = session => Math.max(0, ...[
|
|
21
|
+
session.health?.lastActivityAt,
|
|
22
|
+
session.attention?.requestedAt,
|
|
23
|
+
session.updatedAt,
|
|
24
|
+
session.completedAt,
|
|
25
|
+
session.startedAt,
|
|
26
|
+
].map(value => Date.parse(value || 0)).filter(Number.isFinite));
|
|
27
|
+
const isRecentSession = (session, now = Date.now()) => {
|
|
28
|
+
if (ALWAYS_VISIBLE_STATUSES.has(session.status)) return true;
|
|
29
|
+
const activityAt = sessionActivityTimestamp(session);
|
|
30
|
+
return Boolean(activityAt && Math.max(0, Number(now) - activityAt) <= RECENT_SESSION_WINDOW_MS);
|
|
31
|
+
};
|
|
32
|
+
const hasCurrentRisk = (session, now = Date.now()) => Boolean(
|
|
33
|
+
isRecentSession(session, now)
|
|
34
|
+
&& CURRENT_RISK_STATUSES.has(session.status)
|
|
35
|
+
&& ["critical", "warning"].includes(session.health?.level),
|
|
36
|
+
);
|
|
37
|
+
const needsManagementReview = (session, now = Date.now()) => Boolean(
|
|
38
|
+
isRecentSession(session, now) && (needsUserResponse(session) || hasCurrentRisk(session, now)),
|
|
39
|
+
);
|
|
13
40
|
const prioritySummary = value => {
|
|
14
41
|
const lines = String(value || "")
|
|
15
42
|
.replace(/```[\s\S]*?```/g, " ")
|
|
@@ -37,22 +64,32 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
37
64
|
return readablePreview(firstSentence || t("management.signal_unavailable"), 104).text;
|
|
38
65
|
};
|
|
39
66
|
|
|
40
|
-
function managementBucket(session) {
|
|
41
|
-
if (session
|
|
42
|
-
if (session.health?.level === "
|
|
43
|
-
if (session.
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
67
|
+
function managementBucket(session, now = Date.now()) {
|
|
68
|
+
if (!needsManagementReview(session, now)) return "healthy";
|
|
69
|
+
if (hasCurrentRisk(session, now) && session.health?.level === "critical") return "critical";
|
|
70
|
+
if (hasCurrentRisk(session, now) && session.health?.level === "warning") return "warning";
|
|
71
|
+
if (needsUserResponse(session)) return "attention";
|
|
72
|
+
return "healthy";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function matchesManagementFilter(session, filter, now = Date.now()) {
|
|
76
|
+
if (filter === "critical") return hasCurrentRisk(session, now) && session.health?.level === "critical";
|
|
77
|
+
if (filter === "warning") return hasCurrentRisk(session, now) && session.health?.level === "warning";
|
|
78
|
+
if (filter === "attention") return isRecentSession(session, now) && needsUserResponse(session);
|
|
79
|
+
return needsManagementReview(session, now);
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
function progressHtml(session, compactView = false) {
|
|
49
83
|
const progress = session.progress || { percent: 0, checkpoints: [] };
|
|
50
84
|
const checkpoints = compactView ? [] : (progress.checkpoints || []).slice(-8);
|
|
85
|
+
const completed = Number(progress.completedSteps || 0);
|
|
86
|
+
const total = Number(progress.totalSteps || 0);
|
|
87
|
+
const loggedRatio = total ? Math.round(completed / total * 100) : 0;
|
|
51
88
|
return `<section class="management-progress" data-progress-stage="${esc(progress.stage || "idle")}">
|
|
52
89
|
<header><span>${esc(t("management.progress"))}</span><b>${esc(t("management.progress_steps", {
|
|
53
|
-
completed
|
|
54
|
-
}))}</b><strong>${
|
|
55
|
-
<div class="management-progress-track" role="progressbar" aria-label="${esc(t("management.
|
|
90
|
+
completed, total,
|
|
91
|
+
}))}</b><strong>${completed}/${total}</strong></header>
|
|
92
|
+
<div class="management-progress-track" role="progressbar" aria-label="${esc(t("management.progress_steps", { completed, total }))}" aria-valuemin="0" aria-valuemax="${Math.max(1, total)}" aria-valuenow="${completed}"><i style="width:${Math.max(0, Math.min(100, loggedRatio))}%"></i></div>
|
|
56
93
|
<p><b>${esc(progress.currentStep || t("management.no_current_step"))}</b><span>${esc(progress.lastActivityAt ? t("management.last_signal", { time: timeAgo(progress.lastActivityAt) }) : t("management.signal_unavailable"))}</span></p>
|
|
57
94
|
${progress.blocker ? `<aside><span>${esc(t("management.blocker"))}</span><b>${esc(progress.blocker)}</b></aside>` : ""}
|
|
58
95
|
${checkpoints.length ? `<ol class="management-checkpoints">${checkpoints.map(row => `<li class="${esc(row.status)}"><i></i><div><b>${esc(row.label)}</b>${row.detail ? `<small>${esc(row.detail)}</small>` : ""}</div><span>${esc(row.timestamp ? timeAgo(row.timestamp) : "")}</span></li>`).join("")}</ol>` : ""}
|
|
@@ -63,7 +100,7 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
63
100
|
const health = session.health || { level: "unknown", signals: [] };
|
|
64
101
|
const signals = health.signals || [];
|
|
65
102
|
return `<section class="management-health ${esc(health.level || "unknown")}">
|
|
66
|
-
<header><span>${esc(t("management.health_title"))}</span><b>${esc(healthLabel(health.level))}</b><strong>${
|
|
103
|
+
<header><span>${esc(t("management.health_title"))}</span><b>${esc(healthLabel(health.level))}</b><strong>${esc(t("common.count", { count: signals.length }))}</strong></header>
|
|
67
104
|
${signals.length
|
|
68
105
|
? `<ul>${signals.slice(0, compactView ? 2 : 8).map(signal => `<li class="${esc(signal.severity)}"><i></i><b>${esc(signalLabel(signal.code))}</b>${signal.detail ? `<span title="${esc(signal.detail)}">${esc(signal.detail)}</span>` : ""}</li>`).join("")}</ul>`
|
|
69
106
|
: `<p>${esc(t("management.no_health_signals"))}</p>`}
|
|
@@ -92,7 +129,10 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
92
129
|
if (!attention.required) return "";
|
|
93
130
|
const controls = session.controlCapabilities || {};
|
|
94
131
|
const buttons = [];
|
|
95
|
-
|
|
132
|
+
// A yes/no approval message is only safe for explicit approval requests.
|
|
133
|
+
// Decisions may require selecting a concrete option, so those use the
|
|
134
|
+
// normal instruction composer instead of a misleading generic approval.
|
|
135
|
+
if (controls.sendInstruction && attention.kind === "approval") {
|
|
96
136
|
buttons.push(`<button type="button" class="approve" data-attention-session-id="${esc(session.id)}" data-attention-quick="${esc(t("management.quick.approve_text"))}">${esc(t("management.approve"))}</button>`);
|
|
97
137
|
buttons.push(`<button type="button" data-attention-session-id="${esc(session.id)}" data-attention-quick="${esc(t("management.quick.deny_text"))}">${esc(t("management.deny"))}</button>`);
|
|
98
138
|
}
|
|
@@ -103,10 +143,19 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
103
143
|
function attentionCardHtml(session) {
|
|
104
144
|
const provider = providerInfo(session.provider);
|
|
105
145
|
const attention = session.attention || {};
|
|
146
|
+
const health = session.health || { level: "unknown", signals: [] };
|
|
106
147
|
const evidence = session.evidence || {};
|
|
148
|
+
const cardLabel = attention.required ? attentionLabel(attention.kind) : healthLabel(health.level);
|
|
149
|
+
const statusSignals = (health.signals || []).map(signal => {
|
|
150
|
+
const label = signalLabel(signal.code);
|
|
151
|
+
return signal.detail ? `${label} · ${signal.detail}` : label;
|
|
152
|
+
}).join(" / ");
|
|
153
|
+
const reason = attention.required
|
|
154
|
+
? (attention.summary || session.statusDetail || t("management.response_needed"))
|
|
155
|
+
: (statusSignals || session.statusDetail || t("management.signal_unavailable"));
|
|
107
156
|
return `<article class="attention-card ${esc(attention.kind || "response")}" data-management-session="${esc(session.id)}" style="--management-provider:${provider.accent}">
|
|
108
|
-
<header><span class="provider-mark">${esc(provider.mark)}</span><div><small>${esc(provider.label)} · ${esc(
|
|
109
|
-
<div class="attention-reason"><span>${esc(t("management.requested_action"))}</span><p>${esc(
|
|
157
|
+
<header><span class="provider-mark">${esc(provider.mark)}</span><div><small>${esc(provider.label)} · ${esc(cardLabel)}</small><h3>${esc(session.title)}</h3></div><em class="confidence ${esc(evidence.confidence || "low")}">${esc(evidenceLabel(evidence.confidence))}</em></header>
|
|
158
|
+
<div class="attention-reason"><span>${esc(t(attention.required ? "management.requested_action" : "management.health_title"))}</span><p>${esc(reason)}</p><small>${esc(attention.requestedAt ? timeAgo(attention.requestedAt) : health.lastActivityAt ? timeAgo(health.lastActivityAt) : t("management.signal_unavailable"))}</small></div>
|
|
110
159
|
${progressHtml(session, true)}
|
|
111
160
|
${healthHtml(session, true)}
|
|
112
161
|
${quickActionsHtml(session)}
|
|
@@ -118,22 +167,20 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
118
167
|
function renderAttentionInbox() {
|
|
119
168
|
const section = $("#attentionInbox");
|
|
120
169
|
if (!section) return 0;
|
|
121
|
-
const reviewSessions = context.filteredSessions().filter(
|
|
122
|
-
const filter = ["critical", "warning", "attention"
|
|
123
|
-
const sessions = filter === "all"
|
|
124
|
-
const counts =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
170
|
+
const reviewSessions = context.filteredSessions().filter(needsManagementReview);
|
|
171
|
+
const filter = ["critical", "warning", "attention"].includes(state.managementFilter) ? state.managementFilter : "all";
|
|
172
|
+
const sessions = reviewSessions.filter(session => filter === "all" || matchesManagementFilter(session, filter));
|
|
173
|
+
const counts = {
|
|
174
|
+
critical: reviewSessions.filter(session => matchesManagementFilter(session, "critical")).length,
|
|
175
|
+
warning: reviewSessions.filter(session => matchesManagementFilter(session, "warning")).length,
|
|
176
|
+
attention: reviewSessions.filter(session => matchesManagementFilter(session, "attention")).length,
|
|
177
|
+
};
|
|
129
178
|
const filterButton = (value, label, count) => `<button type="button" data-management-inbox-filter="${value}" aria-pressed="${filter === value ? "true" : "false"}"><i></i><span>${esc(label)}</span><b>${count}</b></button>`;
|
|
130
179
|
section.innerHTML = `<header class="attention-inbox-head"><div><p>${esc(t("management.inbox_eyebrow"))}</p><h2>${esc(t("management.inbox_title"))}</h2><span>${esc(t("management.inbox_description"))}</span></div><strong>${sessions.length}</strong></header>
|
|
131
180
|
<div class="attention-inbox-summary" role="toolbar" aria-label="${esc(t("management.operations_severity_buckets"))}">
|
|
132
|
-
|
|
133
|
-
${filterButton("
|
|
134
|
-
${filterButton("warning", t("management.health.warning"), counts.warning)}
|
|
135
|
-
${filterButton("attention", t("management.health.attention"), counts.attention)}
|
|
136
|
-
${filterButton("unknown", t("management.health.unknown"), counts.unknown)}
|
|
181
|
+
<div class="management-filter-all">${filterButton("all", t("management.filter_all"), reviewSessions.length)}</div>
|
|
182
|
+
<div class="management-filter-group response" role="group" aria-label="${esc(t("management.filter_group_response"))}"><small>${esc(t("management.filter_group_response"))}</small>${filterButton("attention", t("management.health.attention"), counts.attention)}</div>
|
|
183
|
+
<div class="management-filter-group risk" role="group" aria-label="${esc(t("management.filter_group_risk"))}"><small>${esc(t("management.filter_group_risk"))}</small>${filterButton("critical", t("management.health.critical"), counts.critical)}${filterButton("warning", t("management.health.warning"), counts.warning)}</div>
|
|
137
184
|
</div>
|
|
138
185
|
<div class="attention-card-list">${sessions.length ? sessions.map(attentionCardHtml).join("") : `<div class="management-empty"><b>${esc(t("management.inbox_empty"))}</b><span>${esc(t("management.inbox_empty_detail"))}</span></div>`}</div>`;
|
|
139
186
|
return sessions.length;
|
|
@@ -145,17 +192,13 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
145
192
|
const sessions = typeof context.graphFilteredSessions === "function"
|
|
146
193
|
? context.graphFilteredSessions()
|
|
147
194
|
: (state.snapshot?.sessions || []);
|
|
148
|
-
const critical = sessions.filter(session => session
|
|
149
|
-
const warning = sessions.filter(session => session
|
|
150
|
-
const
|
|
151
|
-
const flaggedIds = new Set([...critical, ...warning].map(session => session.id));
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
unknown.forEach(session => flaggedIds.add(session.id));
|
|
156
|
-
const healthy = sessions.filter(session => !flaggedIds.has(session.id) && session.health?.level === "healthy");
|
|
157
|
-
const priority = [...new Map([...critical, ...warning, ...attentionOnly, ...unknown].map(session => [session.id, session])).values()].slice(0, 4);
|
|
158
|
-
const reviewCount = critical.length + warning.length + attentionOnly.length + unknown.length;
|
|
195
|
+
const critical = sessions.filter(session => matchesManagementFilter(session, "critical"));
|
|
196
|
+
const warning = sessions.filter(session => matchesManagementFilter(session, "warning"));
|
|
197
|
+
const responses = sessions.filter(session => matchesManagementFilter(session, "attention"));
|
|
198
|
+
const flaggedIds = new Set([...critical, ...warning, ...responses].map(session => session.id));
|
|
199
|
+
const clear = sessions.filter(session => !flaggedIds.has(session.id));
|
|
200
|
+
const priority = [...new Map([...critical, ...warning, ...responses].map(session => [session.id, session])).values()].slice(0, 4);
|
|
201
|
+
const reviewCount = flaggedIds.size;
|
|
159
202
|
section.innerHTML = `<header>
|
|
160
203
|
<div class="operations-heading">
|
|
161
204
|
<span class="operations-signal" aria-hidden="true">!</span>
|
|
@@ -164,16 +207,16 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
164
207
|
<div class="operations-review-total"><strong>${reviewCount}</strong><span>${esc(t("management.operations_review_count"))}</span></div>
|
|
165
208
|
</header>
|
|
166
209
|
<div class="operations-metrics" aria-label="${esc(t("management.operations_severity_buckets"))}">
|
|
167
|
-
<button type="button" data-management-filter="critical"><span>${esc(t("management.health.critical"))}</span><b>${critical.length}</b></button>
|
|
168
|
-
<button type="button" data-management-filter="warning"><span>${esc(t("management.health.warning"))}</span><b>${warning.length}</b></button>
|
|
169
|
-
<button type="button" data-management-filter="attention"><span>${esc(t("management.health.attention"))}</span><b>${
|
|
170
|
-
<
|
|
171
|
-
<div><span>${esc(t("management.health.healthy"))}</span><b>${healthy.length}</b></div>
|
|
210
|
+
<button type="button" data-management-filter="critical" data-management-metric="critical"><span>${esc(t("management.health.critical"))}</span><b>${critical.length}</b></button>
|
|
211
|
+
<button type="button" data-management-filter="warning" data-management-metric="warning"><span>${esc(t("management.health.warning"))}</span><b>${warning.length}</b></button>
|
|
212
|
+
<button type="button" data-management-filter="attention" data-management-metric="attention"><span>${esc(t("management.health.attention"))}</span><b>${responses.length}</b></button>
|
|
213
|
+
<div data-management-metric="clear"><span>${esc(t("management.recent_clear"))}</span><b>${clear.length}</b></div>
|
|
172
214
|
</div>
|
|
173
215
|
${priority.length ? `<div class="operations-priority">${priority.map(session => {
|
|
174
216
|
const health = session.health || {};
|
|
175
|
-
const
|
|
176
|
-
|
|
217
|
+
const responseSummary = needsUserResponse(session) ? session.attention?.summary : "";
|
|
218
|
+
const summary = prioritySummary(responseSummary
|
|
219
|
+
|| (health.signals || []).map(signal => signalLabel(signal.code)).join(" · ")
|
|
177
220
|
|| t("management.signal_unavailable"));
|
|
178
221
|
return `<button type="button" data-open-session="${esc(session.id)}"><i class="${esc(managementBucket(session))}"></i><span><b>${esc(session.title)}</b><small>${esc(summary)}</small></span><em>${esc(timeAgo(health.lastActivityAt || session.updatedAt))}</em></button>`;
|
|
179
222
|
}).join("")}</div>${reviewCount > priority.length ? `<div class="operations-more"><button type="button" data-management-filter="all"><span>${esc(t("management.operations_view_all"))}</span><b>${reviewCount}</b><i aria-hidden="true">→</i></button></div>` : ""}` : `<p class="operations-clear">${esc(t("management.operations_clear"))}</p>`}`;
|
|
@@ -191,7 +234,7 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
191
234
|
${session.attention?.required ? `<section class="management-attention-detail"><header><span>${esc(attentionLabel(session.attention.kind))}</span><b>${esc(session.attention.summary)}</b></header>${quickActionsHtml(session)}${controls.sendInstruction ? context.agentCommandComposer(session) : ""}</section>` : ""}
|
|
192
235
|
${progressHtml(session)}
|
|
193
236
|
${healthHtml(session)}
|
|
194
|
-
<section class="management-artifacts"><header><span>${esc(t("management.artifacts"))}</span><b>${esc(t("common.items", { count: (outcome.artifacts || []).length }))}</b></header>${outcome.artifacts?.length ? `<ul>${outcome.artifacts.map(item => `<li><i>${esc(item.kind)}</i><b title="${esc(item.value)}">${esc(item.value)}</b><span>${esc(
|
|
237
|
+
<section class="management-artifacts"><header><span>${esc(t("management.artifacts"))}</span><b>${esc(t("common.items", { count: (outcome.artifacts || []).length }))}</b></header>${outcome.artifacts?.length ? `<ul>${outcome.artifacts.map(item => `<li><i>${esc(item.kind)}</i><b title="${esc(item.value)}">${esc(item.value)}</b><span>${esc(t("management.detected"))}</span></li>`).join("")}</ul>` : `<p>${esc(t("management.no_artifacts"))}</p>`}</section>
|
|
195
238
|
<section class="management-checks"><header><span>${esc(t("management.verification_checks"))}</span><b>${esc(t("common.items", { count: (outcome.checks || []).length }))}</b></header>${outcome.checks?.length ? `<ul>${outcome.checks.map(check => `<li class="${esc(check.status)}"><i></i><b>${esc(check.label)}</b><span>${esc(t(`management.check.${check.status}`))}</span></li>`).join("")}</ul>` : `<p>${esc(t("management.no_checks"))}</p>`}</section>
|
|
196
239
|
<section class="management-evidence"><header><span>${esc(t("management.evidence_title"))}</span><b>${esc(evidenceLabel(evidence.confidence))}</b></header><dl><div><dt>${esc(t("management.evidence_status"))}</dt><dd>${esc(evidenceLabel(evidence.status))}</dd></div><div><dt>${esc(t("management.evidence_hierarchy"))}</dt><dd>${esc(evidenceLabel(evidence.hierarchy))}</dd></div><div><dt>${esc(t("management.evidence_completion"))}</dt><dd>${esc(evidenceLabel(evidence.completion))}</dd></div></dl><p>${esc((evidence.sources || []).join(" · ") || t("management.signal_unavailable"))}</p></section>
|
|
197
240
|
<section class="management-controls"><header><span>${esc(t("management.controls"))}</span><b>${esc(t("management.controls_description"))}</b></header>${controlButtonsHtml(session)}</section>
|
|
@@ -199,7 +242,10 @@ window.LoadToAgentAppFactories.createManagement = function createManagement(cont
|
|
|
199
242
|
}
|
|
200
243
|
|
|
201
244
|
return {
|
|
245
|
+
isRecentSession,
|
|
202
246
|
managementBucket,
|
|
247
|
+
matchesManagementFilter,
|
|
248
|
+
needsManagementReview,
|
|
203
249
|
attentionCardHtml,
|
|
204
250
|
controlButtonsHtml,
|
|
205
251
|
healthHtml,
|
|
@@ -83,7 +83,7 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
83
83
|
const active = path === selected;
|
|
84
84
|
return `<button type="button" data-run-workspace="${esc(path)}" class="${active ? "selected" : ""}" title="${esc(path)}" aria-pressed="${active ? "true" : "false"}">
|
|
85
85
|
<span aria-hidden="true">⌘</span>
|
|
86
|
-
${esc(workspace.name || path.split(/[\\/]/).filter(Boolean).pop() || window.LoadToAgentI18n.t("ui.
|
|
86
|
+
${esc(workspace.name || path.split(/[\\/]/).filter(Boolean).pop() || window.LoadToAgentI18n.t("ui.work_folder"))}
|
|
87
87
|
</button>`;
|
|
88
88
|
})
|
|
89
89
|
.join("");
|
package/renderer/app.js
CHANGED
|
@@ -156,7 +156,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
|
|
|
156
156
|
get: (_target, property) => keys[property] ? t(keys[property]) : undefined,
|
|
157
157
|
});
|
|
158
158
|
const STATUS = localizedLookup({
|
|
159
|
-
starting: "ui.preparing", running: "ui.working", paused: "management.status.paused", waiting: "
|
|
159
|
+
starting: "ui.preparing", running: "ui.working", paused: "management.status.paused", waiting: "ui.waiting_for_review", idle: "ui.idle",
|
|
160
160
|
completed: "ui.completed", failed: "ui.problem", cancelled: "ui.stopped",
|
|
161
161
|
});
|
|
162
162
|
const VIEW_TITLES = localizedLookup({
|