tide-commander 1.82.1 → 1.83.0
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/dist/assets/{BossLogsModal-BGN24o2y.js → BossLogsModal-Bgg8MqNZ.js} +1 -1
- package/dist/assets/{BossSpawnModal-BSB9h0Ko.js → BossSpawnModal-SQNEPxH1.js} +1 -1
- package/dist/assets/{ControlsModal-CNHpqsZm.js → ControlsModal-ImEPi76l.js} +1 -1
- package/dist/assets/{DockerLogsModal-BTh78qNq.js → DockerLogsModal-Cs3jjehG.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Dnd_Znj1.js → EmbeddedEditor-Cg3lq_Cv.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-J8iVq9Mn.js → GmailOAuthSetup-1Gh5-yZt.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BUhMeJcp.js → GoogleOAuthSetup-DmXPQyOR.js} +1 -1
- package/dist/assets/{IframeModal-BMghSZpp.js → IframeModal-BEItm7bf.js} +1 -1
- package/dist/assets/{IntegrationsPanel-Bjx4j7mz.js → IntegrationsPanel-epI8eaV_.js} +2 -2
- package/dist/assets/{LogViewerModal-Bi7agxtr.js → LogViewerModal-B4ryb52t.js} +1 -1
- package/dist/assets/{MonitoringModal-CXfskkz6.js → MonitoringModal-CzUjqD5Y.js} +1 -1
- package/dist/assets/{PM2LogsModal-BGp4S4yX.js → PM2LogsModal-Ex9G2Mly.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CIfO0VHD.js → RestoreArchivedAreaModal-DIsamY35.js} +1 -1
- package/dist/assets/{Scene2DCanvas-D3NA3HHw.js → Scene2DCanvas-B5VdT-R2.js} +1 -1
- package/dist/assets/{SceneManager-DaCAID2D.js → SceneManager-CgkTa3Uf.js} +1 -1
- package/dist/assets/{SkillsPanel-L4wQq1xA.js → SkillsPanel-DXxAlZbz.js} +1 -1
- package/dist/assets/{SpawnModal-S5rxcqjR.js → SpawnModal-_1Af5rTQ.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-BfZ-kiW-.js → SubordinateAssignmentModal-CZ1p4KxH.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-C9RFBt0o.js → TriggerManagerPanel-DnOvAQq6.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-Zgcx-wAd.js → WorkflowEditorPanel-Dr896thl.js} +1 -1
- package/dist/assets/{index-0lq0DGnZ.js → index-C70mj1bd.js} +1 -1
- package/dist/assets/{index-DBsSwev4.js → index-CgPa_6TH.js} +1 -1
- package/dist/assets/{index-BVkGtT57.js → index-CkhtIUBY.js} +1 -1
- package/dist/assets/{index-OXP5W30H.js → index-CoUtzDXD.js} +1 -1
- package/dist/assets/{index-DbPVYX_Y.js → index-DCHHa29_.js} +1 -1
- package/dist/assets/{index-Du9hl6DN.js → index-DVqDWfDB.js} +2 -2
- package/dist/assets/{index-DnVqOEVx.js → index-DvRrAd9Y.js} +1 -1
- package/dist/assets/{index-D5Jg-Dl7.js → index-OYmdvKdM.js} +3 -3
- package/dist/assets/{index-KOekme1j.js → index-ave_XgRW.js} +1 -1
- package/dist/assets/{main-DnI9VapU.js → main-CHfGSVnw.js} +94 -93
- package/dist/assets/main-DmmvYoEs.css +1 -0
- package/dist/assets/{web-Bq91XvJE.js → web-D1aYcbd6.js} +1 -1
- package/dist/assets/{web-ttfy3Zlk.js → web-rv0WAVTk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/claude/runner/internal-events.js +6 -3
- package/dist/src/packages/server/claude/runner.js +15 -12
- package/dist/src/packages/server/claude/session-loader.js +240 -0
- package/dist/src/packages/server/routes/agents.js +4 -0
- package/dist/src/packages/server/routes/index.js +2 -0
- package/dist/src/packages/server/routes/sessions.js +93 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +13 -0
- package/dist/src/packages/server/utils/index.js +1 -0
- package/dist/src/packages/server/utils/log-context.js +10 -0
- package/dist/src/packages/server/utils/logger.js +6 -1
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +17 -5
- package/package.json +1 -1
- package/dist/assets/main-DrNeyjdE.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cg as a}from"./main-
|
|
1
|
+
import{cg as a}from"./main-CHfGSVnw.js";import{ImpactStyle as i,NotificationType as r}from"./index-DVqDWfDB.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cg as s}from"./main-
|
|
1
|
+
import{cg as s}from"./main-CHfGSVnw.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-CHfGSVnw.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-DmmvYoEs.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withAgentContext } from '../../utils/log-context.js';
|
|
1
2
|
export class RunnerInternalEventBus {
|
|
2
3
|
handlers = new Map();
|
|
3
4
|
on(type, handler) {
|
|
@@ -15,8 +16,10 @@ export class RunnerInternalEventBus {
|
|
|
15
16
|
if (!handlers || handlers.size === 0) {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
handler
|
|
20
|
-
|
|
19
|
+
withAgentContext(event.agentId, () => {
|
|
20
|
+
for (const handler of handlers) {
|
|
21
|
+
handler(event);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { ClaudeBackend } from './backend.js';
|
|
6
6
|
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { withAgentContext } from '../utils/log-context.js';
|
|
7
8
|
import { isProcessRunning } from '../data/index.js';
|
|
8
9
|
import { sendToTmux, hasTmuxSession, tmuxSessionName, getTmuxPanePid } from './runner/tmux-helper.js';
|
|
9
10
|
import { spawn } from 'child_process';
|
|
@@ -333,9 +334,12 @@ export class ClaudeRunner {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
async run(request) {
|
|
336
|
-
await this.lifecycle.run(request);
|
|
337
|
+
await withAgentContext(request.agentId, () => this.lifecycle.run(request));
|
|
337
338
|
}
|
|
338
339
|
sendMessage(agentId, message) {
|
|
340
|
+
return withAgentContext(agentId, () => this.sendMessageImpl(agentId, message));
|
|
341
|
+
}
|
|
342
|
+
sendMessageImpl(agentId, message) {
|
|
339
343
|
const activeProcess = this.activeProcesses.get(agentId);
|
|
340
344
|
if (!activeProcess) {
|
|
341
345
|
log.error(`❌ [SEND_MESSAGE] No active process for agent ${agentId}`);
|
|
@@ -571,23 +575,22 @@ export class ClaudeRunner {
|
|
|
571
575
|
log.log(`✅ [TMUX] Reconnected to tmux session ${sessionName} for agent ${agentId}`);
|
|
572
576
|
}
|
|
573
577
|
interrupt(agentId) {
|
|
574
|
-
return this.lifecycle.interrupt(agentId);
|
|
578
|
+
return withAgentContext(agentId, () => this.lifecycle.interrupt(agentId));
|
|
575
579
|
}
|
|
576
580
|
getActiveProcessCount() {
|
|
577
581
|
return this.activeProcesses.size;
|
|
578
582
|
}
|
|
579
583
|
async stop(agentId, clearQueue = true) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
log.log(`🗑️ [QUEUE] Agent ${agentId}: Discarded ${count} queued message(s) on stop`);
|
|
584
|
+
await withAgentContext(agentId, async () => {
|
|
585
|
+
if (clearQueue && this.messageQueue.has(agentId)) {
|
|
586
|
+
const count = this.messageQueue.get(agentId).length;
|
|
587
|
+
this.messageQueue.delete(agentId);
|
|
588
|
+
if (count > 0) {
|
|
589
|
+
log.log(`🗑️ [QUEUE] Agent ${agentId}: Discarded ${count} queued message(s) on stop`);
|
|
590
|
+
}
|
|
588
591
|
}
|
|
589
|
-
|
|
590
|
-
|
|
592
|
+
await this.lifecycle.stop(agentId);
|
|
593
|
+
});
|
|
591
594
|
}
|
|
592
595
|
async stopAll(killProcesses = true, clearQueue = true) {
|
|
593
596
|
if (this.persistTimer) {
|
|
@@ -417,6 +417,246 @@ export async function listSessions(cwd) {
|
|
|
417
417
|
sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
418
418
|
return sessions;
|
|
419
419
|
}
|
|
420
|
+
const FIRST_PROMPT_MAX_LEN = 240;
|
|
421
|
+
const SEARCH_SNIPPET_MAX_LEN = 280;
|
|
422
|
+
function truncate(text, max) {
|
|
423
|
+
if (text.length <= max)
|
|
424
|
+
return text;
|
|
425
|
+
return text.slice(0, max - 1) + '…';
|
|
426
|
+
}
|
|
427
|
+
function extractClaudeMessageText(message) {
|
|
428
|
+
if (!message || typeof message !== 'object')
|
|
429
|
+
return '';
|
|
430
|
+
const m = message;
|
|
431
|
+
const content = m.content;
|
|
432
|
+
if (typeof content === 'string')
|
|
433
|
+
return content;
|
|
434
|
+
if (Array.isArray(content)) {
|
|
435
|
+
const parts = [];
|
|
436
|
+
for (const block of content) {
|
|
437
|
+
if (block && typeof block === 'object' && 'type' in block && block.type === 'text') {
|
|
438
|
+
const text = block.text;
|
|
439
|
+
if (typeof text === 'string')
|
|
440
|
+
parts.push(text);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return parts.join('\n');
|
|
444
|
+
}
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Read the first up-to-`maxLines` JSONL records of a session file. Used to
|
|
449
|
+
* cheaply recover `cwd` and the first user prompt without parsing the whole
|
|
450
|
+
* conversation. Falls back to a single readFileSync slice for very small files.
|
|
451
|
+
*/
|
|
452
|
+
async function readSessionHeader(filePath, maxLines = 20) {
|
|
453
|
+
let cwd = '';
|
|
454
|
+
let firstPrompt = '';
|
|
455
|
+
try {
|
|
456
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
457
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
458
|
+
let lineCount = 0;
|
|
459
|
+
for await (const line of rl) {
|
|
460
|
+
if (!line.trim())
|
|
461
|
+
continue;
|
|
462
|
+
lineCount += 1;
|
|
463
|
+
try {
|
|
464
|
+
const obj = JSON.parse(line);
|
|
465
|
+
if (!cwd && typeof obj.cwd === 'string')
|
|
466
|
+
cwd = obj.cwd;
|
|
467
|
+
// queue-operation enqueue rows carry the user's prompt as `content`
|
|
468
|
+
if (!firstPrompt && obj.type === 'queue-operation' && obj.operation === 'enqueue') {
|
|
469
|
+
const content = obj.content;
|
|
470
|
+
if (typeof content === 'string')
|
|
471
|
+
firstPrompt = content;
|
|
472
|
+
}
|
|
473
|
+
if (!firstPrompt && obj.type === 'user') {
|
|
474
|
+
const text = extractClaudeMessageText(obj.message);
|
|
475
|
+
if (text)
|
|
476
|
+
firstPrompt = text;
|
|
477
|
+
}
|
|
478
|
+
if (cwd && firstPrompt)
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// ignore malformed lines
|
|
483
|
+
}
|
|
484
|
+
if (lineCount >= maxLines)
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
rl.close();
|
|
488
|
+
stream.destroy();
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// unreadable file — return empty defaults
|
|
492
|
+
}
|
|
493
|
+
return { cwd, firstPrompt: truncate(firstPrompt, FIRST_PROMPT_MAX_LEN) };
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* List every Claude session across every project directory.
|
|
497
|
+
*
|
|
498
|
+
* Cheap: only stats each .jsonl and reads the first few lines for cwd/firstPrompt.
|
|
499
|
+
* Skips message-count computation by default to keep the operation fast even
|
|
500
|
+
* across hundreds of sessions.
|
|
501
|
+
*/
|
|
502
|
+
export async function listAllSessions(options) {
|
|
503
|
+
if (!fs.existsSync(PROJECTS_DIR))
|
|
504
|
+
return [];
|
|
505
|
+
const projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
506
|
+
const all = [];
|
|
507
|
+
for (const projectDir of projectDirs) {
|
|
508
|
+
const fullProjectPath = path.join(PROJECTS_DIR, projectDir);
|
|
509
|
+
let entries;
|
|
510
|
+
try {
|
|
511
|
+
entries = fs.readdirSync(fullProjectPath, { withFileTypes: true });
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
518
|
+
continue;
|
|
519
|
+
const sessionId = entry.name.replace(/\.jsonl$/, '');
|
|
520
|
+
const filePath = path.join(fullProjectPath, entry.name);
|
|
521
|
+
let stats;
|
|
522
|
+
try {
|
|
523
|
+
stats = fs.statSync(filePath);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
all.push({
|
|
529
|
+
sessionId,
|
|
530
|
+
projectPath: '',
|
|
531
|
+
projectDir,
|
|
532
|
+
lastModified: stats.mtime,
|
|
533
|
+
messageCount: 0,
|
|
534
|
+
firstPrompt: '',
|
|
535
|
+
sizeBytes: stats.size,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
all.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
540
|
+
const limit = options?.limit ?? all.length;
|
|
541
|
+
const limited = all.slice(0, limit);
|
|
542
|
+
// Enrich with cwd + firstPrompt (and optionally messageCount). Run concurrently
|
|
543
|
+
// but cap concurrency so we don't open hundreds of file handles at once.
|
|
544
|
+
const concurrency = 8;
|
|
545
|
+
let cursor = 0;
|
|
546
|
+
const workers = [];
|
|
547
|
+
for (let i = 0; i < concurrency; i++) {
|
|
548
|
+
workers.push((async () => {
|
|
549
|
+
while (cursor < limited.length) {
|
|
550
|
+
const idx = cursor++;
|
|
551
|
+
const item = limited[idx];
|
|
552
|
+
const filePath = path.join(PROJECTS_DIR, item.projectDir, `${item.sessionId}.jsonl`);
|
|
553
|
+
const header = await readSessionHeader(filePath);
|
|
554
|
+
item.projectPath = header.cwd;
|
|
555
|
+
item.firstPrompt = header.firstPrompt;
|
|
556
|
+
if (options?.includeMessageCount) {
|
|
557
|
+
try {
|
|
558
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
559
|
+
const lines = content.split('\n');
|
|
560
|
+
let count = 0;
|
|
561
|
+
for (const line of lines) {
|
|
562
|
+
if (!line.trim())
|
|
563
|
+
continue;
|
|
564
|
+
try {
|
|
565
|
+
const obj = JSON.parse(line);
|
|
566
|
+
if (obj.type === 'user' || obj.type === 'assistant')
|
|
567
|
+
count++;
|
|
568
|
+
}
|
|
569
|
+
catch { /* ignore */ }
|
|
570
|
+
}
|
|
571
|
+
item.messageCount = count;
|
|
572
|
+
}
|
|
573
|
+
catch { /* ignore */ }
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
})());
|
|
577
|
+
}
|
|
578
|
+
await Promise.all(workers);
|
|
579
|
+
return limited;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Full-text search every Claude session for `query`. Streams files line-by-line
|
|
583
|
+
* so memory usage stays bounded even with many large sessions.
|
|
584
|
+
*/
|
|
585
|
+
export async function searchAllSessions(query, options) {
|
|
586
|
+
const trimmed = query.trim();
|
|
587
|
+
if (!trimmed)
|
|
588
|
+
return [];
|
|
589
|
+
const queryLower = trimmed.toLowerCase();
|
|
590
|
+
const limit = options?.limit ?? 100;
|
|
591
|
+
const cwdFilter = options?.cwdFilter?.toLowerCase();
|
|
592
|
+
const sessions = await listAllSessions({ limit: 1000 });
|
|
593
|
+
const out = [];
|
|
594
|
+
const concurrency = 6;
|
|
595
|
+
let cursor = 0;
|
|
596
|
+
const workers = [];
|
|
597
|
+
for (let i = 0; i < concurrency; i++) {
|
|
598
|
+
workers.push((async () => {
|
|
599
|
+
while (cursor < sessions.length) {
|
|
600
|
+
const idx = cursor++;
|
|
601
|
+
const session = sessions[idx];
|
|
602
|
+
if (cwdFilter && !session.projectPath.toLowerCase().includes(cwdFilter))
|
|
603
|
+
continue;
|
|
604
|
+
const filePath = path.join(PROJECTS_DIR, session.projectDir, `${session.sessionId}.jsonl`);
|
|
605
|
+
let totalMatches = 0;
|
|
606
|
+
let snippet = '';
|
|
607
|
+
try {
|
|
608
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
609
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
610
|
+
for await (const line of rl) {
|
|
611
|
+
if (!line)
|
|
612
|
+
continue;
|
|
613
|
+
// Skip the cheap path: just lowercase the raw line and substring search.
|
|
614
|
+
// This avoids parsing every JSON record but matches inside any field
|
|
615
|
+
// (message text, tool args, etc.) — fine for a UX search.
|
|
616
|
+
if (!line.toLowerCase().includes(queryLower))
|
|
617
|
+
continue;
|
|
618
|
+
totalMatches++;
|
|
619
|
+
if (!snippet) {
|
|
620
|
+
// try to extract a clean text snippet from message content
|
|
621
|
+
try {
|
|
622
|
+
const obj = JSON.parse(line);
|
|
623
|
+
if (obj.type === 'user' || obj.type === 'assistant') {
|
|
624
|
+
const text = extractClaudeMessageText(obj.message);
|
|
625
|
+
if (text)
|
|
626
|
+
snippet = text;
|
|
627
|
+
}
|
|
628
|
+
else if (obj.type === 'queue-operation' && typeof obj.content === 'string') {
|
|
629
|
+
snippet = obj.content;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch { /* fall through to raw-line snippet */ }
|
|
633
|
+
if (!snippet)
|
|
634
|
+
snippet = line;
|
|
635
|
+
snippet = truncate(snippet.replace(/\s+/g, ' ').trim(), SEARCH_SNIPPET_MAX_LEN);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
rl.close();
|
|
639
|
+
stream.destroy();
|
|
640
|
+
}
|
|
641
|
+
catch { /* unreadable — skip */ }
|
|
642
|
+
if (totalMatches > 0) {
|
|
643
|
+
out.push({
|
|
644
|
+
sessionId: session.sessionId,
|
|
645
|
+
projectPath: session.projectPath,
|
|
646
|
+
projectDir: session.projectDir,
|
|
647
|
+
lastModified: session.lastModified,
|
|
648
|
+
totalMatches,
|
|
649
|
+
snippet,
|
|
650
|
+
firstPrompt: session.firstPrompt,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
})());
|
|
655
|
+
}
|
|
656
|
+
await Promise.all(workers);
|
|
657
|
+
out.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
658
|
+
return out.slice(0, limit);
|
|
659
|
+
}
|
|
420
660
|
/**
|
|
421
661
|
* Wait for file to be stable (not actively being written)
|
|
422
662
|
* Checks if file size remains constant over a small interval
|
|
@@ -13,6 +13,7 @@ import { loadSession } from '../claude/session-loader.js';
|
|
|
13
13
|
import { getAllCustomClasses } from '../services/custom-class-service.js';
|
|
14
14
|
// Session listing is done inline for performance
|
|
15
15
|
import { createLogger } from '../utils/logger.js';
|
|
16
|
+
import { withAgentContext } from '../utils/log-context.js';
|
|
16
17
|
import { truncateOrEmpty } from '../utils/string.js';
|
|
17
18
|
import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js';
|
|
18
19
|
import { clearDelegation, getBossForSubordinate } from '../websocket/handlers/boss-response-handler.js';
|
|
@@ -21,6 +22,9 @@ import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnable
|
|
|
21
22
|
import { getBackupStatus, setBackupEnabled } from '../services/backup-service.js';
|
|
22
23
|
const log = createLogger('Routes');
|
|
23
24
|
const router = Router();
|
|
25
|
+
router.param('id', (req, _res, next, id) => {
|
|
26
|
+
withAgentContext(typeof id === 'string' ? id : undefined, () => next());
|
|
27
|
+
});
|
|
24
28
|
// Store for broadcasting via WebSocket
|
|
25
29
|
let broadcastFn = null;
|
|
26
30
|
/**
|
|
@@ -22,6 +22,7 @@ import triggerRouter, { setBroadcast as setTriggerBroadcast } from './trigger-ro
|
|
|
22
22
|
import integrationRouter from './integration-routes.js';
|
|
23
23
|
import eventRouter from './event-routes.js';
|
|
24
24
|
import workflowRouter from './workflow-routes.js';
|
|
25
|
+
import sessionsRouter from './sessions.js';
|
|
25
26
|
import { getPlugins } from '../integrations/integration-registry.js';
|
|
26
27
|
const router = Router();
|
|
27
28
|
// Health check
|
|
@@ -46,6 +47,7 @@ router.use('/triggers', triggerRouter);
|
|
|
46
47
|
router.use('/integrations', integrationRouter);
|
|
47
48
|
router.use('/events', eventRouter);
|
|
48
49
|
router.use('/workflows', workflowRouter);
|
|
50
|
+
router.use('/sessions', sessionsRouter);
|
|
49
51
|
// Integration plugin routes (e.g. /api/slack/*, /api/documents/*, /api/jira/*)
|
|
50
52
|
// Uses lazy lookup so plugins can be registered after route setup
|
|
51
53
|
router.use((req, res, next) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions Routes
|
|
3
|
+
*
|
|
4
|
+
* Cross-project (cwd-agnostic) session discovery and preview. Pairs with the
|
|
5
|
+
* global Session Finder modal in the client. Restoring a session onto an agent
|
|
6
|
+
* is still done over the WebSocket `restore_session` message, which now accepts
|
|
7
|
+
* an optional `cwd` to support cross-project attach.
|
|
8
|
+
*/
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import { listAllSessions, searchAllSessions, loadSession } from '../claude/session-loader.js';
|
|
11
|
+
import { createLogger } from '../utils/logger.js';
|
|
12
|
+
const log = createLogger('Routes');
|
|
13
|
+
const router = Router();
|
|
14
|
+
// GET /api/sessions/global?limit=200&includeMessageCount=false
|
|
15
|
+
router.get('/global', async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const limit = Math.max(1, Math.min(parseInt(req.query.limit) || 200, 1000));
|
|
18
|
+
const includeMessageCount = req.query.includeMessageCount === 'true';
|
|
19
|
+
const sessions = await listAllSessions({ limit, includeMessageCount });
|
|
20
|
+
res.json({
|
|
21
|
+
sessions: sessions.map((s) => ({
|
|
22
|
+
sessionId: s.sessionId,
|
|
23
|
+
projectPath: s.projectPath,
|
|
24
|
+
projectDir: s.projectDir,
|
|
25
|
+
lastModified: s.lastModified.toISOString(),
|
|
26
|
+
messageCount: s.messageCount,
|
|
27
|
+
firstPrompt: s.firstPrompt,
|
|
28
|
+
sizeBytes: s.sizeBytes,
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
log.error(' Failed to list global sessions:', err);
|
|
34
|
+
res.status(500).json({ error: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// GET /api/sessions/search?q=...&limit=100&cwdFilter=...
|
|
38
|
+
router.get('/search', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const q = req.query.q || '';
|
|
41
|
+
if (!q.trim()) {
|
|
42
|
+
res.status(400).json({ error: 'Query parameter "q" is required' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const limit = Math.max(1, Math.min(parseInt(req.query.limit) || 100, 500));
|
|
46
|
+
const cwdFilter = req.query.cwdFilter || undefined;
|
|
47
|
+
const matches = await searchAllSessions(q, { limit, cwdFilter });
|
|
48
|
+
res.json({
|
|
49
|
+
query: q,
|
|
50
|
+
matches: matches.map((m) => ({
|
|
51
|
+
sessionId: m.sessionId,
|
|
52
|
+
projectPath: m.projectPath,
|
|
53
|
+
projectDir: m.projectDir,
|
|
54
|
+
lastModified: m.lastModified.toISOString(),
|
|
55
|
+
totalMatches: m.totalMatches,
|
|
56
|
+
snippet: m.snippet,
|
|
57
|
+
firstPrompt: m.firstPrompt,
|
|
58
|
+
})),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
log.error(' Failed to search global sessions:', err);
|
|
63
|
+
res.status(500).json({ error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// GET /api/sessions/preview?cwd=...&sessionId=...&limit=30
|
|
67
|
+
router.get('/preview', async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const cwd = req.query.cwd || '';
|
|
70
|
+
const sessionId = req.query.sessionId || '';
|
|
71
|
+
if (!cwd || !sessionId) {
|
|
72
|
+
res.status(400).json({ error: 'Both "cwd" and "sessionId" query parameters are required' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const limit = Math.max(1, Math.min(parseInt(req.query.limit) || 30, 200));
|
|
76
|
+
const history = await loadSession(cwd, sessionId, limit, 0);
|
|
77
|
+
if (!history) {
|
|
78
|
+
res.status(404).json({ error: 'Session not found' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.json({
|
|
82
|
+
sessionId: history.sessionId,
|
|
83
|
+
cwd: history.cwd,
|
|
84
|
+
messages: history.messages,
|
|
85
|
+
totalCount: history.totalCount,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log.error(' Failed to load session preview:', err);
|
|
90
|
+
res.status(500).json({ error: err.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
export default router;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import * as agentService from './agent-service.js';
|
|
2
|
+
import { withAgentContext } from '../utils/log-context.js';
|
|
2
3
|
import { clearPendingSilentContextRefresh, clearStdinWatchdog, hasPendingSilentContextRefresh, markPendingSilentContextRefresh, startStdinWatchdog, } from './runtime-watchdog.js';
|
|
3
4
|
export function createRuntimeCommandExecution(deps) {
|
|
4
5
|
const { log, getRunner, getRunnerForAgent, notifyCommandStarted, emitOutput, killDetachedProviderProcessInCwd, } = deps;
|
|
5
6
|
async function executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent, silent, skipNotify) {
|
|
7
|
+
return withAgentContext(agentId, () => executeCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent, silent, skipNotify));
|
|
8
|
+
}
|
|
9
|
+
async function executeCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent, silent, skipNotify) {
|
|
6
10
|
const agent = agentService.getAgent(agentId);
|
|
7
11
|
if (!agent) {
|
|
8
12
|
throw new Error(`Agent not found: ${agentId}`);
|
|
@@ -59,6 +63,9 @@ export function createRuntimeCommandExecution(deps) {
|
|
|
59
63
|
});
|
|
60
64
|
}
|
|
61
65
|
async function sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent) {
|
|
66
|
+
return withAgentContext(agentId, () => sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent));
|
|
67
|
+
}
|
|
68
|
+
async function sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent) {
|
|
62
69
|
const agent = agentService.getAgent(agentId);
|
|
63
70
|
if (!agent) {
|
|
64
71
|
throw new Error(`Agent not found: ${agentId}`);
|
|
@@ -181,6 +188,9 @@ export function createRuntimeCommandExecution(deps) {
|
|
|
181
188
|
await executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent);
|
|
182
189
|
}
|
|
183
190
|
async function sendSilentCommand(agentId, command) {
|
|
191
|
+
return withAgentContext(agentId, () => sendSilentCommandImpl(agentId, command));
|
|
192
|
+
}
|
|
193
|
+
async function sendSilentCommandImpl(agentId, command) {
|
|
184
194
|
const agent = agentService.getAgent(agentId);
|
|
185
195
|
if (!agent) {
|
|
186
196
|
throw new Error(`Agent not found: ${agentId}`);
|
|
@@ -210,6 +220,9 @@ export function createRuntimeCommandExecution(deps) {
|
|
|
210
220
|
await executeCommand(agentId, command, undefined, undefined, undefined, true);
|
|
211
221
|
}
|
|
212
222
|
async function stopAgent(agentId) {
|
|
223
|
+
return withAgentContext(agentId, () => stopAgentImpl(agentId));
|
|
224
|
+
}
|
|
225
|
+
async function stopAgentImpl(agentId) {
|
|
213
226
|
// Cancel any pending stdin watchdog timer to prevent it from respawning
|
|
214
227
|
// the process after we've stopped it
|
|
215
228
|
clearStdinWatchdog(agentId);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const als = new AsyncLocalStorage();
|
|
3
|
+
export function withAgentContext(agentId, fn) {
|
|
4
|
+
if (!agentId)
|
|
5
|
+
return fn();
|
|
6
|
+
return als.run({ agentId }, fn);
|
|
7
|
+
}
|
|
8
|
+
export function getCurrentAgentId() {
|
|
9
|
+
return als.getStore()?.agentId;
|
|
10
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { getCurrentAgentId } from './log-context.js';
|
|
8
9
|
const fileLogConfig = {
|
|
9
10
|
enabled: true,
|
|
10
11
|
directory: path.join(process.cwd(), 'logs'),
|
|
@@ -180,6 +181,10 @@ function formatMessage(level, context, message, ...args) {
|
|
|
180
181
|
const timestampStr = `${colors.dim}${timestamp}${colors.reset}`;
|
|
181
182
|
const levelStr = `${color}${colors.bright}${label.padStart(7)}${colors.reset}`;
|
|
182
183
|
const contextStr = `${colors.yellow}[${context}]${colors.reset}`;
|
|
184
|
+
const agentId = getCurrentAgentId();
|
|
185
|
+
const agentStr = agentId
|
|
186
|
+
? ` ${colors.cyan}[agentId=${agentId}]${colors.reset}`
|
|
187
|
+
: '';
|
|
183
188
|
let formattedMessage = message;
|
|
184
189
|
if (args.length > 0) {
|
|
185
190
|
const argsStr = args.map(arg => {
|
|
@@ -195,7 +200,7 @@ function formatMessage(level, context, message, ...args) {
|
|
|
195
200
|
}).join(' ');
|
|
196
201
|
formattedMessage = `${message} ${colors.cyan}${argsStr}${colors.reset}`;
|
|
197
202
|
}
|
|
198
|
-
return `${appName} ${pidStr} - ${timestampStr} ${levelStr} ${contextStr} ${formattedMessage}`;
|
|
203
|
+
return `${appName} ${pidStr} - ${timestampStr} ${levelStr} ${contextStr}${agentStr} ${formattedMessage}`;
|
|
199
204
|
}
|
|
200
205
|
class Logger {
|
|
201
206
|
context;
|
|
@@ -175,6 +175,11 @@ export async function handleClearContext(ctx, payload) {
|
|
|
175
175
|
}
|
|
176
176
|
/**
|
|
177
177
|
* Handle restore_session message - restores a previous session for an agent
|
|
178
|
+
*
|
|
179
|
+
* Optional `cwd` enables cross-project restore from the global Session Finder:
|
|
180
|
+
* if the chosen session lives in a different project directory than the agent
|
|
181
|
+
* currently uses, we update the agent's cwd alongside its sessionId so Claude
|
|
182
|
+
* Code can resolve the JSONL on the next run.
|
|
178
183
|
*/
|
|
179
184
|
export async function handleRestoreSession(ctx, payload) {
|
|
180
185
|
const agent = agentService.getAgent(payload.agentId);
|
|
@@ -182,13 +187,14 @@ export async function handleRestoreSession(ctx, payload) {
|
|
|
182
187
|
ctx.sendError(`Agent not found: ${payload.agentId}`);
|
|
183
188
|
return;
|
|
184
189
|
}
|
|
185
|
-
|
|
190
|
+
const targetCwd = payload.cwd && payload.cwd !== agent.cwd ? payload.cwd : undefined;
|
|
191
|
+
log.log(`Agent ${agent.name}: Restoring session ${payload.sessionId}` +
|
|
192
|
+
(targetCwd ? ` (cwd change: ${agent.cwd} -> ${targetCwd})` : ''));
|
|
186
193
|
// Archive the current session first (if any)
|
|
187
194
|
agentService.archiveCurrentSession(payload.agentId);
|
|
188
195
|
// Stop any running process
|
|
189
196
|
await runtimeService.stopAgent(payload.agentId);
|
|
190
|
-
|
|
191
|
-
agentService.updateAgent(payload.agentId, {
|
|
197
|
+
const updates = {
|
|
192
198
|
status: 'idle',
|
|
193
199
|
currentTask: undefined,
|
|
194
200
|
taskLabel: undefined,
|
|
@@ -197,8 +203,14 @@ export async function handleRestoreSession(ctx, payload) {
|
|
|
197
203
|
tokensUsed: 0,
|
|
198
204
|
contextUsed: 0,
|
|
199
205
|
contextStats: undefined,
|
|
200
|
-
}
|
|
201
|
-
|
|
206
|
+
};
|
|
207
|
+
if (targetCwd) {
|
|
208
|
+
updates.cwd = targetCwd;
|
|
209
|
+
}
|
|
210
|
+
agentService.updateAgent(payload.agentId, updates);
|
|
211
|
+
ctx.sendActivity(payload.agentId, targetCwd
|
|
212
|
+
? `Session restored from ${targetCwd} - will resume on next command`
|
|
213
|
+
: `Session restored - will resume on next command`);
|
|
202
214
|
log.log(`Agent ${agent.name}: Session restored to ${payload.sessionId}`);
|
|
203
215
|
}
|
|
204
216
|
/**
|