Skip to content

Commit 43cc03a

Browse files
author
StackMemory Bot (CLI)
committed
feat(conductor): add files view, phase filter to monitor TUI
- [f] key shows git status per worktree (modified/added/deleted files) - [1-5] keys filter by phase (reading/planning/implementing/testing/committing) - [0] clears phase filter - --phase flag for CLI filtering - workspacePath persisted in agent status.json for file inspection - Color-coded file status (yellow=modified, green=added, red=deleted)
1 parent 8b422c2 commit 43cc03a

File tree

2 files changed

+110
-3
lines changed

2 files changed

+110
-3
lines changed

src/cli/commands/orchestrate.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -665,17 +665,26 @@ export function createConductorCommands(): Command {
665665
.command('monitor')
666666
.description('Interactive TUI dashboard for conductor monitoring')
667667
.option('--interval <seconds>', 'Auto-refresh interval in seconds', '10')
668+
.option(
669+
'--phase <phase>',
670+
'Filter by phase (reading, planning, implementing, testing, committing)'
671+
)
668672
.option('--no-interactive', 'Disable interactive keys (CI/pipe mode)')
669673
.action(async (options) => {
670674
const interval = parseInt(options.interval, 10) * 1000;
671675
const interactive = options.interactive !== false;
672-
let currentMode: 'dashboard' | 'status' | 'usage' | 'json' = 'dashboard';
676+
let currentMode: 'dashboard' | 'status' | 'usage' | 'json' | 'files' =
677+
'dashboard';
673678
let paused = false;
674679
let refreshInterval = interval;
680+
let phaseFilter: string | null = options.phase || null;
675681

676682
const b = '\x1b[1m';
677683
const d = '\x1b[2m';
678684
const cyan = '\x1b[36m';
685+
const green = '\x1b[32m';
686+
const yellow = '\x1b[33m';
687+
const red = '\x1b[31m';
679688
const r = '\x1b[0m';
680689

681690
function readStatuses(): AgentStatusFile[] {
@@ -702,12 +711,17 @@ export function createConductorCommands(): Command {
702711
(a, b) =>
703712
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
704713
);
714+
// Apply phase filter
715+
if (phaseFilter) {
716+
return statuses.filter((s) => s.phase === phaseFilter);
717+
}
705718
return statuses;
706719
}
707720

708721
function printStatusTable(statuses: AgentStatusFile[]): void {
709722
if (statuses.length === 0) {
710-
console.log(' No active agents');
723+
const filterNote = phaseFilter ? ` (filter: ${phaseFilter})` : '';
724+
console.log(` No active agents${filterNote}`);
711725
return;
712726
}
713727
const header = `${'Issue'.padEnd(12)}${'Phase'.padEnd(16)}${'Tools'.padStart(7)}${'Files'.padStart(7)}${'Tokens'.padStart(9)} Last Update`;
@@ -719,6 +733,62 @@ export function createConductorCommands(): Command {
719733
}
720734
}
721735

736+
function getWorktreeFiles(workspacePath: string): string[] {
737+
if (!workspacePath || !existsSync(workspacePath)) return [];
738+
try {
739+
const output = execSync('git status --short 2>/dev/null', {
740+
cwd: workspacePath,
741+
timeout: 5000,
742+
encoding: 'utf-8',
743+
});
744+
return output
745+
.trim()
746+
.split('\n')
747+
.filter((l) => l.length > 0);
748+
} catch {
749+
return [];
750+
}
751+
}
752+
753+
function printFilesView(statuses: AgentStatusFile[]): void {
754+
if (statuses.length === 0) {
755+
const filterNote = phaseFilter ? ` (filter: ${phaseFilter})` : '';
756+
console.log(` No active agents${filterNote}`);
757+
return;
758+
}
759+
for (const s of statuses) {
760+
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
761+
const phaseColor =
762+
s.phase === 'committing'
763+
? green
764+
: s.phase === 'testing'
765+
? yellow
766+
: s.phase === 'implementing'
767+
? cyan
768+
: '';
769+
console.log(
770+
`${b}${s.issue}${r} ${phaseColor}${s.phase}${r} ${d}${formatElapsed(elapsed)}${r}`
771+
);
772+
773+
const files = getWorktreeFiles(s.workspacePath || '');
774+
if (files.length === 0) {
775+
console.log(` ${d}(no file changes detected)${r}`);
776+
} else {
777+
for (const f of files) {
778+
const status = f.substring(0, 2);
779+
const path = f.substring(3);
780+
let color = '';
781+
if (status.includes('M')) color = yellow;
782+
else if (status.includes('A') || status.includes('?'))
783+
color = green;
784+
else if (status.includes('D')) color = red;
785+
console.log(` ${color}${status}${r} ${path}`);
786+
}
787+
}
788+
console.log('');
789+
}
790+
}
791+
722792
async function getUsage(): Promise<Record<string, unknown>> {
723793
const conductor = new Conductor({ repoRoot: process.cwd() });
724794
await conductor.scanUsageLogs();
@@ -740,11 +810,15 @@ export function createConductorCommands(): Command {
740810
console.log(
741811
` Mode: ${cyan}${currentMode}${r} | Refresh: ${intervalSec}s`
742812
);
813+
const filterNote = phaseFilter
814+
? ` Filter: ${cyan}${phaseFilter}${r}`
815+
: '';
743816
if (interactive) {
744817
console.log(
745-
` ${d}[s]tatus [u]sage [d]ashboard [j]son [l]ogs [r]efresh [p]ause [+/-] [q]uit${r}`
818+
` ${d}[s]tatus [u]sage [f]iles [d]ashboard [j]son [l]ogs [r]efresh [p]ause [1-5]phase [0]clear [+/-] [q]uit${r}`
746819
);
747820
}
821+
if (filterNote) console.log(filterNote);
748822
console.log(
749823
`${b}══════════════════════════════════════════════════${r}`
750824
);
@@ -773,6 +847,9 @@ export function createConductorCommands(): Command {
773847
console.log(JSON.stringify(usage, null, 2));
774848
break;
775849
}
850+
case 'files':
851+
printFilesView(statuses);
852+
break;
776853
}
777854

778855
console.log('');
@@ -835,10 +912,38 @@ export function createConductorCommands(): Command {
835912
currentMode = 'dashboard';
836913
await render();
837914
break;
915+
case 'f':
916+
currentMode = 'files';
917+
await render();
918+
break;
838919
case 'j':
839920
currentMode = 'json';
840921
await render();
841922
break;
923+
case '1':
924+
phaseFilter = 'reading';
925+
await render();
926+
break;
927+
case '2':
928+
phaseFilter = 'planning';
929+
await render();
930+
break;
931+
case '3':
932+
phaseFilter = 'implementing';
933+
await render();
934+
break;
935+
case '4':
936+
phaseFilter = 'testing';
937+
await render();
938+
break;
939+
case '5':
940+
phaseFilter = 'committing';
941+
await render();
942+
break;
943+
case '0':
944+
phaseFilter = null;
945+
await render();
946+
break;
842947
case 'l': {
843948
// Show log picker
844949
process.stdout.write('\x1b[2J\x1b[H');

src/cli/commands/orchestrator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export interface AgentStatusFile {
158158
filesModified: number;
159159
toolCalls: number;
160160
tokensUsed: number;
161+
workspacePath?: string;
161162
}
162163

163164
// ── Helpers ──
@@ -650,6 +651,7 @@ export class Conductor {
650651
filesModified: run.filesModified,
651652
toolCalls: run.toolCalls,
652653
tokensUsed: run.tokensUsed,
654+
workspacePath: run.workspacePath || undefined,
653655
};
654656
writeFileSync(join(dir, 'status.json'), JSON.stringify(status, null, 2));
655657
} catch {

0 commit comments

Comments
 (0)