loadtoagent 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -81,6 +81,18 @@ LoadToAgent는 시작할 때 현재 패키지 버전과 GitHub의 최신 정식
81
81
 
82
82
  홈의 `10분 시작 가이드`도 같은 네 단계를 직접 눌러 연습하게 해 줍니다. 완료 상태는 이 컴퓨터에 저장되며 언제든 다시 펼칠 수 있습니다.
83
83
 
84
+ ### 대화를 이어 보내고 실제 전달 상태 확인하기
85
+
86
+ 작업의 **대화 내용** 탭에서 **내용을 이어서 보내기**를 사용하면 지원되는 연결 세션에 새 메시지를 보낼 수 있습니다. 별도 채팅을 만들지 않고 같은 작업과 같은 AI 세션에서 대화가 이어집니다.
87
+
88
+ LoadToAgent는 전달 상태를 실제로 관측한 세 단계로 보여 줍니다.
89
+
90
+ 1. 앱에서 전송 요청을 보냈는지
91
+ 2. 같은 사용자 메시지가 AI 세션 기록에 나타났는지
92
+ 3. 그 메시지 뒤에 새 AI 응답 시작 신호가 나타났는지
93
+
94
+ 12초 안에 세션 기록에서 수신이 확인되지 않으면 AI가 응답을 준비한다고 단정하지 않고 `전달 확인 지연`으로 표시합니다. AI의 중간 진행 문장도 별도 상자로 분리하지 않고 시간순 대화 안에 그대로 보여 주며, 마지막 완료 메시지에만 **최종 답변** 표시를 붙입니다. 모든 상태 문구는 로컬 세션 파일에서 확인한 근거를 설명하며 외부 AI 창의 동작을 추측하지 않습니다.
95
+
84
96
  ## 한눈에 볼 수 있는 것
85
97
 
86
98
  | 화면 | 확인할 수 있는 내용 |
@@ -89,7 +101,7 @@ LoadToAgent는 시작할 때 현재 패키지 버전과 GitHub의 최신 정식
89
101
  | 연결 관계 | 사용자 요청, 선택한 메인 AI, 직접 나눠 맡긴 도움 AI |
90
102
  | 명령·백그라운드 실행 | AI가 시작한 일반 명령과 백그라운드 명령의 내용, 작업 폴더, 실행 ID와 현재 상태 |
91
103
  | 응답·상태 확인함 | 최근 24시간 세션에서 내 응답이 필요한 요청과 현재 실행의 실패·지연·일시정지 위험만 확인 |
92
- | 작업 상세 | 대화, 도구 호출, 진행 과정, 모델, 작업 폴더, 상태 |
104
+ | 작업 상세 | 시간순 대화, 근거가 확인된 전달 상태, 도구 호출, 진행 과정, 모델, 작업 폴더, 상태 |
93
105
  | 관리 요약 | 최근 실행 이벤트, 상태 정보 확인 수준, 완료 신호, 로그에서 찾은 산출물 후보와 테스트 기록, 실행 제어 |
94
106
  | 토큰 | 입력·출력·캐시·추론·전체 사용량과 보고된 컨텍스트 점유율 |
95
107
  | AI 세션 터미널 | 선택한 AI의 이전 대화와 기존 PTY·tmux 화면을 나란히 보며 같은 세션에 이어서 입력 |
package/README.md CHANGED
@@ -81,6 +81,18 @@ On startup, LoadToAgent compares its package version with the newest stable GitH
81
81
 
82
82
  The `10-minute start guide` on Home lets you practice the same four steps. Progress is saved on this computer and the guide can be reopened at any time.
83
83
 
84
+ ### Continue a conversation and verify delivery
85
+
86
+ Open a task's **Conversation** tab and use **Continue the conversation** to send another message to a supported connected session. The message stays in the same task and session instead of opening a separate chat.
87
+
88
+ LoadToAgent shows delivery as three observable steps:
89
+
90
+ 1. the app dispatched the send request;
91
+ 2. the exact user message appeared in the AI session log;
92
+ 3. a new AI response-start signal appeared after that message.
93
+
94
+ If the session log does not confirm receipt within 12 seconds, the app reports a delivery-confirmation delay instead of claiming that the AI is already preparing a response. Intermediate AI updates remain in chronological chat order, and only the latest completed message is marked **Final answer**. These labels describe local evidence from the session files; they do not guess what an external AI window is doing.
95
+
84
96
  ## What LoadToAgent shows
85
97
 
86
98
  | View | What you get |
@@ -89,7 +101,7 @@ The `10-minute start guide` on Home lets you practice the same four steps. Progr
89
101
  | Relationship view | The request origin, selected agent, and every directly delegated subagent |
90
102
  | Execution units | Foreground shells, background shells, and background jobs started by an AI, including command, workspace, execution ID, and live status |
91
103
  | Operations and attention inbox | Prioritized failures, stalls, context risk, approvals, decisions, and input requests with immediate actions |
92
- | Session detail | Conversation, tool activity, lifecycle events, model, workspace, and status |
104
+ | Session detail | Chronological conversation, evidence-based delivery state, tool activity, lifecycle events, model, workspace, and status |
93
105
  | Management summary | Checkpoints, observation confidence, completion summary, artifacts, verification, and run controls |
94
106
  | Token view | Input, output, cached, reasoning, total, and reported context-window usage |
95
107
  | Session terminal | The selected AI's prior conversation beside its existing PTY or tmux pane, with input continuing in that exact session |
package/README.zh-CN.md CHANGED
@@ -81,6 +81,18 @@ LoadToAgent 启动时会比较当前包版本与最新的稳定 GitHub Release
81
81
 
82
82
  首页的`10 分钟入门指南`可以带你实际完成同样的四个步骤。进度只保存在本机,并且可随时重新打开。
83
83
 
84
+ ### 继续对话并确认实际传递状态
85
+
86
+ 打开任务的**对话**页,使用**继续对话**即可向受支持的已连接会话发送新消息。消息会保留在同一个任务和 AI 会话中,不会另开聊天。
87
+
88
+ LoadToAgent 会按照实际可观测证据显示三个传递步骤:
89
+
90
+ 1. 应用是否已发出发送请求;
91
+ 2. 相同的用户消息是否已出现在 AI 会话记录中;
92
+ 3. 该消息之后是否出现了新的 AI 回复开始信号。
93
+
94
+ 如果 12 秒内仍未在会话记录中确认收到消息,应用会显示“传递确认延迟”,而不会直接声称 AI 正在准备回复。AI 的中间进度消息也会按时间顺序保留在对话中,不再放入单独的折叠区域;只有最后一条已完成消息会标记为**最终回答**。所有状态文字都说明本地会话文件中的可验证证据,不会猜测外部 AI 窗口正在做什么。
95
+
84
96
  ## LoadToAgent 可以展示什么
85
97
 
86
98
  | 视图 | 内容 |
@@ -89,7 +101,7 @@ LoadToAgent 启动时会比较当前包版本与最新的稳定 GitHub Release
89
101
  | 关系视图 | 用户请求、当前代理以及它直接委派的所有子代理 |
90
102
  | 执行单元 | AI 启动的前台 Shell、后台 Shell 与后台任务,包括命令、工作目录、执行 ID 和实时状态 |
91
103
  | 运行概览与待确认收件箱 | 按优先级集中展示失败、停滞、上下文风险、审批、决策和输入请求,并可立即处理 |
92
- | 会话详情 | 对话、工具活动、执行过程、模型、工作目录和状态 |
104
+ | 会话详情 | 按时间排列的对话、基于证据的传递状态、工具活动、执行过程、模型、工作目录和状态 |
93
105
  | 管理摘要 | 检查点、观测置信度、完成摘要、产物、验证结果和执行控制 |
94
106
  | Token 视图 | 输入、输出、缓存、推理、总量和已报告的上下文占用率 |
95
107
  | 终端控制 | 本地 Shell,以及向 LoadToAgent 自有终端安全发送命令 |
package/main.js CHANGED
@@ -523,7 +523,7 @@ async function setupRuntime() {
523
523
 
524
524
  function bridgePresence() {
525
525
  if (!terminalManager) return [];
526
- const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
526
+ const localEnvironment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
527
527
  return terminalManager.list()
528
528
  .filter(session => !session.transient && session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
529
529
  .map(session => ({
@@ -535,7 +535,8 @@ function bridgePresence() {
535
535
  pid: session.pid,
536
536
  cwd: session.cwd,
537
537
  startedAt: session.createdAt,
538
- environment,
538
+ environment: session.distro && process.platform === 'win32' ? 'wsl' : localEnvironment,
539
+ distro: session.distro || '',
539
540
  kind: 'bridge',
540
541
  label: 'LoadToAgent 외부 터미널 브리지',
541
542
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtoagent",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
5
5
  "main": "main.js",
6
6
  "author": "wincube AX",
@@ -43,6 +43,7 @@
43
43
  "scripts": {
44
44
  "start": "electron .",
45
45
  "test": "node scripts/regression-test.js",
46
+ "test:accuracy": "node scripts/accuracy-evaluation.js",
46
47
  "test:terminal": "electron scripts/terminal-integration-test.js",
47
48
  "test:terminal:mac-stress": "node scripts/mac-terminal-stress-check.js",
48
49
  "test:terminal-restart": "node scripts/terminal-restart-persistence-check.js",
@@ -12,6 +12,7 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
12
12
  selectView,
13
13
  providerInfo,
14
14
  isLiveSession,
15
+ conversationMessageKey,
15
16
  } = context;
16
17
 
17
18
  function agentCommandTargets(session) {
@@ -91,6 +92,54 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
91
92
  return session?.parentId ? `${session.id}:${route}` : session.id;
92
93
  }
93
94
 
95
+ function beginConversationMessage(session, command) {
96
+ const detail = state.details.get(session.id);
97
+ const baselineMessages = [...(session.messages || []), ...(detail?.messages || [])];
98
+ const entry = {
99
+ id: `local:${session.id}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
100
+ text: command,
101
+ timestamp: new Date().toISOString(),
102
+ status: "sending",
103
+ phase: "sending",
104
+ dispatchedAt: null,
105
+ presented: false,
106
+ baselineMessageKeys: new Set(baselineMessages.map(conversationMessageKey)),
107
+ };
108
+ const pending = state.pendingConversationMessages.get(session.id) || [];
109
+ pending.push(entry);
110
+ state.pendingConversationMessages.set(session.id, pending);
111
+ state.drawerForceLatest = true;
112
+ context.renderDrawer?.();
113
+ return entry;
114
+ }
115
+
116
+ function updateConversationMessage(sessionId, entry, status, error = "") {
117
+ if (!entry) return;
118
+ entry.status = status;
119
+ entry.error = error;
120
+ if (status === "awaiting") {
121
+ entry.dispatchedAt = entry.dispatchedAt || new Date().toISOString();
122
+ entry.phase = "confirming";
123
+ const delay = Number(window.LoadToAgentConversationDelivery?.CONFIRMATION_DELAY_MS || 12_000);
124
+ clearTimeout(entry.confirmationTimer);
125
+ entry.confirmationTimer = setTimeout(() => {
126
+ entry.confirmationTimer = 0;
127
+ const pending = state.pendingConversationMessages.get(sessionId) || [];
128
+ if (!pending.includes(entry) || entry.status !== "awaiting") return;
129
+ state.drawerForceLatest = true;
130
+ context.render?.();
131
+ context.renderDrawer?.();
132
+ }, delay + 40);
133
+ } else if (status === "failed") {
134
+ entry.phase = "failed";
135
+ entry.failedAt = new Date().toISOString();
136
+ clearTimeout(entry.confirmationTimer);
137
+ entry.confirmationTimer = 0;
138
+ }
139
+ state.drawerForceLatest = true;
140
+ context.renderDrawer?.();
141
+ }
142
+
94
143
  function agentControlMode(session, targets) {
95
144
  if (targets.length && isLiveSession(session)) return "direct";
96
145
  const resume = agentResumeSupport(session);
@@ -194,7 +243,7 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
194
243
  const placeholder = editable ? t(options.conversation ? "agent.conversation_placeholder" : "agent.command_example") : status;
195
244
  const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
196
245
  return `<form class="agent-command-panel ${availabilityClass} control-${mode} ${options.conversation ? "conversation-composer" : ""}"
197
- data-agent-command-form="${esc(session.id)}" data-agent-command-route-selected="${esc(route)}" data-agent-command-routing="${routingEnabled ? "conversation" : "session"}">
246
+ data-agent-command-form="${esc(session.id)}" data-agent-command-route-selected="${esc(route)}" data-agent-command-routing="${options.conversation ? "conversation" : "session"}">
198
247
  <header>
199
248
  <span class="agent-command-icon" aria-hidden="true">›_</span>
200
249
  <span><b>${esc(t(options.conversation ? "agent.conversation_title" : "agent.command_title"))}</b><small>${esc(status)}</small></span>
@@ -255,7 +304,8 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
255
304
  if (state.agentCommandSending.has(sessionId)) return;
256
305
  const session = snapshotSession(sessionId);
257
306
  if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.latest_not_found"));
258
- const routingEnabled = form?.dataset.agentCommandRouting === "conversation" && Boolean(session.parentId);
307
+ const conversationSubmission = form?.dataset.agentCommandRouting === "conversation";
308
+ const routingEnabled = conversationSubmission && Boolean(session.parentId);
259
309
  const requestedRoute = routingEnabled ? form?.dataset.agentCommandRouteSelected || selectedAgentCommandRoute(session) : "direct";
260
310
  const routeContext = routingEnabled
261
311
  ? routedAgentCommandContext(session, requestedRoute)
@@ -274,26 +324,20 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
274
324
  if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
275
325
  if (input) state.agentCommandDrafts.set(sessionId, input.value);
276
326
  if (!command) return context.toast(t("agent.enter_command"));
277
- if (routingEnabled && routeContext.route === "parent") {
327
+ if (conversationSubmission) {
278
328
  state.agentCommandSending.add(sessionId);
279
- const submit = form.querySelector('[type="submit"]');
280
- if (submit) {
281
- submit.disabled = true;
282
- submit.textContent = t("agent.sending");
283
- }
329
+ const pendingMessage = beginConversationMessage(session, command);
284
330
  try {
285
331
  await window.LoadToAgentTerminal.resumeForAgent(targetSession, routedCommand, true, { focus: false });
286
332
  state.agentCommandDrafts.delete(sessionId);
287
- if (input) input.value = "";
288
- context.toast(t("agent.command_routed_via_parent"));
333
+ updateConversationMessage(sessionId, pendingMessage, "awaiting");
334
+ context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent_background"));
289
335
  } catch (error) {
336
+ updateConversationMessage(sessionId, pendingMessage, "failed", errorText(error, "agent.send_failed"));
290
337
  context.toast(errorText(error, "agent.send_failed"));
291
338
  } finally {
292
339
  state.agentCommandSending.delete(sessionId);
293
- if (submit && submit.isConnected) {
294
- submit.disabled = false;
295
- submit.textContent = t("agent.send_via_parent");
296
- }
340
+ context.renderDrawer?.();
297
341
  }
298
342
  return;
299
343
  }
@@ -307,6 +351,8 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
307
351
  return context.toast(t(agentCommandTargets(session).length ? "agent.select_target_first" : "agent.no_writable_terminal"));
308
352
  if (!command) return context.toast(t("agent.enter_command"));
309
353
  state.agentCommandSending.add(sessionId);
354
+ if (input) state.agentCommandDrafts.set(sessionId, input.value);
355
+ const pendingMessage = conversationSubmission ? beginConversationMessage(session, command) : null;
310
356
  const submit = form.querySelector('[type="submit"]');
311
357
  if (submit) {
312
358
  submit.disabled = true;
@@ -316,28 +362,33 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
316
362
  await window.LoadToAgentTerminal.dispatchAgentCommand(targetSession, routedCommand, target.id);
317
363
  state.agentCommandDrafts.delete(sessionId);
318
364
  if (input) input.value = "";
365
+ updateConversationMessage(sessionId, pendingMessage, "awaiting");
319
366
  context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent", { target: target.label }));
320
367
  } catch (error) {
321
- if (session.parentId) {
322
- context.toast(errorText(error, "agent.send_failed"));
323
- return;
324
- }
325
- const latest = snapshotSession(sessionId) || session;
368
+ const latest = snapshotSession(targetSession.id) || targetSession;
326
369
  const support = agentResumeSupport(latest);
327
- if (!agentCommandTargets(latest).length && support.supported) {
370
+ const shouldRecover = support.supported
371
+ && (conversationSubmission || !agentCommandTargets(latest).length);
372
+ if (shouldRecover) {
328
373
  try {
329
374
  state.agentCommandDrafts.set(sessionId, command);
330
- await window.LoadToAgentTerminal.resumeForAgent(latest, command, true);
375
+ await window.LoadToAgentTerminal.resumeForAgent(latest, routedCommand, true, { focus: conversationSubmission ? false : true });
331
376
  state.agentCommandDrafts.delete(sessionId);
332
377
  if (input) input.value = "";
378
+ updateConversationMessage(sessionId, pendingMessage, "awaiting");
333
379
  context.toast(t("agent.recovered_and_sent"));
334
380
  return;
335
381
  } catch (resumeError) {
382
+ updateConversationMessage(sessionId, pendingMessage, "failed", errorText(resumeError, "agent.recovery_failed"));
336
383
  context.toast(errorText(resumeError, "agent.recovery_failed"));
337
384
  }
338
- } else context.toast(errorText(error, "agent.send_failed"));
385
+ } else {
386
+ updateConversationMessage(sessionId, pendingMessage, "failed", errorText(error, "agent.send_failed"));
387
+ context.toast(errorText(error, "agent.send_failed"));
388
+ }
339
389
  } finally {
340
390
  state.agentCommandSending.delete(sessionId);
391
+ if (conversationSubmission) context.renderDrawer?.();
341
392
  if (submit && submit.isConnected) {
342
393
  submit.disabled = false;
343
394
  submit.textContent = t(routingEnabled && routeContext.route === "parent" ? "agent.send_via_parent" : "agent.send_now");
@@ -51,7 +51,11 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
51
51
  }
52
52
 
53
53
  function normalizedProjectPath(value) {
54
- return String(value || "").trim().replace(/\\/g, "/").replace(/\/+$/, "").toLocaleLowerCase();
54
+ let normalized = String(value || "").trim().replace(/\\/g, "/");
55
+ normalized = normalized.replace(/^\/\/\?\/([a-z]:\/)/i, "$1");
56
+ const wslWindowsMount = normalized.match(/^\/mnt\/([a-z])(?:\/(.*))?$/i);
57
+ if (wslWindowsMount) normalized = `${wslWindowsMount[1]}:/${wslWindowsMount[2] || ""}`;
58
+ return normalized.replace(/\/+$/, "").toLocaleLowerCase();
55
59
  }
56
60
 
57
61
  function projectContainsPath(projectPath, candidatePath) {
@@ -67,8 +71,15 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
67
71
 
68
72
  function observedProjects(sessions = displaySessions().filter((session) => !session.parentId)) {
69
73
  const projects = new Map();
70
- const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
71
- saved.forEach((item) => projects.set(item.key, { path: item.path, name: item.name || projectName(item.path), saved: true, count: 0 }));
74
+ const saved = state.workspaces.map((item, index) => ({ ...item, key: normalizedProjectPath(item.path), order: index }));
75
+ saved.forEach((item) => projects.set(item.key, {
76
+ path: item.path,
77
+ name: item.name || projectName(item.path),
78
+ saved: true,
79
+ order: item.order,
80
+ count: 0,
81
+ liveCount: 0,
82
+ }));
72
83
  sessions.filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
73
84
  const originPath = sessionOriginPath(session);
74
85
  if (!originPath) return;
@@ -77,8 +88,16 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
77
88
  .sort((a, b) => b.key.length - a.key.length)[0];
78
89
  const path = owner ? owner.path : originPath;
79
90
  const key = normalizedProjectPath(path);
80
- const project = projects.get(key) || { path, name: projectName(path), saved: false, count: 0 };
91
+ const project = projects.get(key) || {
92
+ path,
93
+ name: projectName(path),
94
+ saved: false,
95
+ order: Number.MAX_SAFE_INTEGER,
96
+ count: 0,
97
+ liveCount: 0,
98
+ };
81
99
  project.count += 1;
100
+ if (isControlRoomSession(session)) project.liveCount += 1;
82
101
  project.lastActivityAt = !project.lastActivityAt || Date.parse(session.updatedAt || 0) > Date.parse(project.lastActivityAt || 0)
83
102
  ? session.updatedAt
84
103
  : project.lastActivityAt;
@@ -93,7 +112,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
93
112
  item.name = parts.slice(-2).join("/") || item.name;
94
113
  });
95
114
  return items.sort((a, b) => Number(b.count || 0) - Number(a.count || 0)
96
- || Number(b.saved) - Number(a.saved)
115
+ || Number(a.order ?? Number.MAX_SAFE_INTEGER) - Number(b.order ?? Number.MAX_SAFE_INTEGER)
97
116
  || String(a.name).localeCompare(String(b.name), uiLocale()));
98
117
  }
99
118
 
@@ -130,24 +149,20 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
130
149
  const rootSessions = displaySessions().filter((session) => !session.parentId);
131
150
  const liveRootSessions = rootSessions.filter(isControlRoomSession);
132
151
  const projects = observedProjects(rootSessions);
133
- const savedProjectOrder = new Map(state.workspaces.map((workspace, index) => [normalizedProjectPath(workspace.path), index]));
134
- const liveProjects = observedProjects(liveRootSessions).sort((a, b) => {
135
- const aOrder = savedProjectOrder.get(normalizedProjectPath(a.path));
136
- const bOrder = savedProjectOrder.get(normalizedProjectPath(b.path));
137
- if (aOrder != null || bOrder != null) return (aOrder ?? Number.MAX_SAFE_INTEGER) - (bOrder ?? Number.MAX_SAFE_INTEGER);
138
- return Number(b.count || 0) - Number(a.count || 0) || String(a.name).localeCompare(String(b.name), uiLocale());
139
- });
152
+ const liveProjects = observedProjects(liveRootSessions);
140
153
  const projectlessCount = rootSessions.filter(isProjectlessSession).length;
141
154
  const liveProjectlessCount = liveRootSessions.filter(isProjectlessSession).length;
142
155
  const savedWorkspaceExists = state.workspace === "all"
143
156
  || (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
144
157
  || projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
145
158
  if (!savedWorkspaceExists) state.workspace = "all";
146
- const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${state.workspace === item.path ? "selected" : ""}"
159
+ const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${Number(item.liveCount || 0) ? "has-live-sessions" : ""} ${state.workspace === item.path ? "selected" : ""}"
147
160
  data-workspace="${esc(item.path)}" title="${esc(item.path)}"
161
+ data-live-session-count="${Number(item.liveCount || 0)}"
148
162
  aria-label="${esc(t("project.filter_named", { name: item.name, count: item.count }))}"
149
163
  aria-pressed="${state.workspace === item.path ? "true" : "false"}">
150
164
  <strong>${esc(item.name)}</strong><small>${Number(item.count || 0)}</small>
165
+ ${Number(item.liveCount || 0) ? `<span class="workspace-live-state" title="${esc(t("project.in_progress"))}"><i aria-hidden="true"></i><b>${esc(t("project.in_progress"))}</b></span>` : ""}
151
166
  </button>`;
152
167
  const mobileHtml =
153
168
  `<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
@@ -551,6 +566,32 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
551
566
  return true;
552
567
  }
553
568
 
569
+ function ensureProjectOrder(projectKeys = []) {
570
+ if (!Array.isArray(state.projectOrder)) state.projectOrder = [];
571
+ const known = new Set(state.projectOrder);
572
+ for (const value of projectKeys) {
573
+ const key = String(value || "");
574
+ if (!key || known.has(key)) continue;
575
+ state.projectOrder.push(key);
576
+ known.add(key);
577
+ }
578
+ return state.projectOrder;
579
+ }
580
+
581
+ function moveProjectOrder(sourceId, targetId, placeAfter = false) {
582
+ const source = String(sourceId || "");
583
+ const target = String(targetId || "");
584
+ if (!source || !target || source === target) return false;
585
+ const order = ensureProjectOrder([source, target]);
586
+ const sourceIndex = order.indexOf(source);
587
+ if (sourceIndex < 0 || !order.includes(target)) return false;
588
+ order.splice(sourceIndex, 1);
589
+ const targetIndex = order.indexOf(target);
590
+ order.splice(targetIndex + (placeAfter ? 1 : 0), 0, source);
591
+ state.projectOrder = order;
592
+ return true;
593
+ }
594
+
554
595
  function graphFilteredSessions() {
555
596
  let sessions = displaySessions();
556
597
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
@@ -607,6 +648,8 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
607
648
  graphFilteredSessions,
608
649
  stableSessionSort,
609
650
  moveSessionOrder,
651
+ ensureProjectOrder,
652
+ moveProjectOrder,
610
653
  renderProviderVisibilitySettings,
611
654
  };
612
655
  };