Skip to content

Commit 9525149

Browse files
committed
feat(core): add console messenger to TUI
1 parent e5d2e71 commit 9525149

17 files changed

+1519
-163
lines changed

Cargo.lock

Lines changed: 543 additions & 67 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/nx/Cargo.toml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ opt-level = "z"
1212
strip = "none"
1313

1414
[dependencies]
15+
tokio = { version = "1.44.0", features = [
16+
"sync",
17+
"macros",
18+
"io-util",
19+
"rt",
20+
"time",
21+
] }
1522
anyhow = "1.0.71"
1623
better-panic = "0.3.0"
1724
colored = "2"
@@ -51,14 +58,17 @@ terminal-colorsaurus = "0.4.0"
5158
thiserror = "1.0.40"
5259
tracing = "0.1.37"
5360
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
54-
tokio = { version = "1.32.0", features = ['sync','macros','io-util','rt','time'] }
5561
tokio-util = "0.7.9"
5662
tracing-appender = "0.2"
5763
tui-logger = { version = "0.17.2", features = ["tracing-support"] }
5864
tui-term = { git = "https://github.com/JamesHenry/tui-term", rev = "88e3b61425c97220c528ef76c188df10032a75dd" }
5965
walkdir = '2.3.3'
6066
xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] }
6167
vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" }
68+
serde = "1.0.219"
69+
serde_json = "1.0.140"
70+
71+
sha2 = "0.10.8"
6272

6373
[target.'cfg(windows)'.dependencies]
6474
winapi = { version = "0.3", features = ["fileapi", "psapi", "shellapi"] }
@@ -74,13 +84,22 @@ portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1
7484
ignore-files = "2.1.0"
7585
fs4 = "0.12.0"
7686
ratatui = { version = "0.29", features = ["scrolling-regions"] }
77-
reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls"] }
87+
reqwest = { version = "0.12.15", default-features = false, features = [
88+
"rustls-tls",
89+
] }
7890
rusqlite = { version = "0.32.1", features = ["bundled", "array", "vtab"] }
7991
watchexec = "3.0.1"
8092
watchexec-events = "2.0.1"
8193
watchexec-filterer-ignore = "3.0.0"
8294
watchexec-signals = "2.1.0"
8395
machine-uid = "0.5.2"
96+
interprocess = { version = "2.2.3", features = ["tokio"] }
97+
jsonrpsee = { version = "0.25.1", features = [
98+
"client-core",
99+
"async-client",
100+
"macros",
101+
"http-client",
102+
] }
84103

85104
[lib]
86105
crate-type = ['cdylib']
@@ -94,3 +113,4 @@ insta = "1.42.2"
94113
# This is only used for unit tests
95114
swc_ecma_dep_graph = "0.109.1"
96115
tempfile = "3.13.0"
116+
uuid = { version = "1.0", features = ["v4"] }

packages/nx/src/native/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export declare class ExternalObject<T> {
88
}
99
}
1010
export declare class AppLifeCycle {
11-
constructor(tasks: Array<Task>, initiatingTasks: Array<string>, runMode: RunMode, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string)
11+
constructor(tasks: Array<Task>, initiatingTasks: Array<string>, runMode: RunMode, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string, workspaceRoot: string)
1212
startCommand(threadCount?: number | undefined | null): void
1313
scheduleTask(task: Task): void
1414
startTasks(tasks: Array<Task>, metadata: object): void
@@ -18,7 +18,7 @@ export declare class AppLifeCycle {
1818
__init(doneCallback: () => any): void
1919
registerRunningTask(taskId: string, parserAndWriter: ExternalObject<[ParserArc, WriterArc]>): void
2020
registerRunningTaskWithEmptyParser(taskId: string): void
21-
appendTaskOutput(taskId: string, output: string, isPtyOutput: boolean): void
21+
appendTaskOutput(taskId: string, output: string): void
2222
setTaskStatus(taskId: string, status: TaskStatus): void
2323
registerForcedShutdownCallback(forcedShutdownCallback: () => any): void
2424
__setCloudMessage(message: string): Promise<void>

packages/nx/src/native/index.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,15 @@ const originalLoad = Module._load;
6262
// Will only be called once because the require cache takes over afterwards.
6363
Module._load = function (request, parent, isMain) {
6464
const modulePath = request;
65-
if (
65+
// Check if we should use the native file cache (enabled by default)
66+
const useNativeFileCache = process.env.NX_SKIP_NATIVE_FILE_CACHE !== 'true';
67+
// Check if this is an Nx native module (either from npm or local file)
68+
const isNxNativeModule =
6669
nxPackages.has(modulePath) ||
67-
localNodeFiles.some((f) => modulePath.endsWith(f))
68-
) {
70+
localNodeFiles.some((file) => modulePath.endsWith(file));
71+
72+
// Only use the file cache for Nx native modules when caching is enabled
73+
if (useNativeFileCache && isNxNativeModule) {
6974
const nativeLocation = require.resolve(modulePath);
7075
const fileName = basename(nativeLocation);
7176

packages/nx/src/native/tui/action.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ pub enum Action {
3535
StartTasks(Vec<Task>),
3636
EndTasks(Vec<TaskResult>),
3737
ToggleDebugMode,
38+
SendConsoleMessage(String),
39+
ConsoleMessagesAvailable(bool),
3840
}

packages/nx/src/native/tui/app.rs

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use crate::native::{
2323
tasks::types::{Task, TaskResult},
2424
};
2525

26-
use super::action::Action;
2726
use super::components::Component;
2827
use super::components::countdown_popup::CountdownPopup;
2928
use super::components::help_popup::HelpPopup;
@@ -39,6 +38,7 @@ use super::pty::PtyInstance;
3938
use super::theme::THEME;
4039
use super::tui;
4140
use super::utils::normalize_newlines;
41+
use super::{action::Action, nx_console::messaging::NxConsoleMessageConnection};
4242

4343
pub struct App {
4444
pub components: Vec<Box<dyn Component>>,
@@ -69,6 +69,7 @@ pub struct App {
6969
tasks: Vec<Task>,
7070
debug_mode: bool,
7171
debug_state: TuiWidgetState,
72+
console_messenger: Option<NxConsoleMessageConnection>,
7273
}
7374

7475
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -136,6 +137,7 @@ impl App {
136137
tasks,
137138
debug_mode: false,
138139
debug_state: TuiWidgetState::default(),
140+
console_messenger: None,
139141
})
140142
}
141143

@@ -208,6 +210,10 @@ impl App {
208210

209211
// Show countdown popup for the configured duration (making sure the help popup is not open first)
210212
pub fn end_command(&mut self) {
213+
self.console_messenger
214+
.as_ref()
215+
.and_then(|c| c.end_running_tasks());
216+
211217
// If the user has interacted with the app, or auto-exit is disabled, do nothing
212218
if self.user_has_interacted || !self.tui_config.auto_exit.should_exit_automatically() {
213219
return;
@@ -308,6 +314,10 @@ impl App {
308314
&& !(matches!(self.focus, Focus::MultipleOutput(_))
309315
&& self.is_interactive_mode())
310316
{
317+
self.console_messenger
318+
.as_ref()
319+
.and_then(|c| c.end_running_tasks());
320+
311321
self.is_forced_shutdown = true;
312322
// Quit immediately
313323
self.quit_at = Some(std::time::Instant::now());
@@ -747,8 +757,25 @@ impl App {
747757
trace!("{action:?}");
748758
}
749759
match &action {
760+
Action::StartCommand(_) => {
761+
self.console_messenger
762+
.as_ref()
763+
.and_then(|c| c.start_running_tasks());
764+
}
765+
Action::Tick => {
766+
self.console_messenger.as_ref().and_then(|messenger| {
767+
self.components
768+
.iter()
769+
.find_map(|c| c.as_any().downcast_ref::<TasksList>())
770+
.and_then(|tasks_list| {
771+
messenger.update_running_tasks(&tasks_list.tasks, &self.pty_instances)
772+
})
773+
});
774+
}
750775
// Quit immediately
751-
Action::Quit => self.quit_at = Some(std::time::Instant::now()),
776+
Action::Quit => {
777+
self.quit_at = Some(std::time::Instant::now());
778+
}
752779
// Cancel quitting
753780
Action::CancelQuit => {
754781
self.quit_at = None;
@@ -958,6 +985,7 @@ impl App {
958985
is_focused,
959986
has_pty,
960987
is_next_tab_target,
988+
self.console_messenger.is_some(),
961989
);
962990

963991
let terminal_pane = TerminalPane::new()
@@ -984,6 +1012,13 @@ impl App {
9841012
})
9851013
.ok();
9861014
}
1015+
Action::SendConsoleMessage(msg) => {
1016+
if let Some(connection) = &self.console_messenger {
1017+
connection.send_terminal_string(msg);
1018+
} else {
1019+
trace!("No console connection available");
1020+
}
1021+
}
9871022
_ => {}
9881023
}
9891024

@@ -1034,10 +1069,11 @@ impl App {
10341069

10351070
/// Dispatches an action to the action tx for other components to handle however they see fit
10361071
fn dispatch_action(&self, action: Action) {
1037-
let tx = self.action_tx.clone().unwrap();
1038-
tokio::spawn(async move {
1039-
let _ = tx.send(action);
1040-
});
1072+
if let Some(tx) = &self.action_tx {
1073+
tx.send(action).unwrap_or_else(|e| {
1074+
debug!("Failed to dispatch action: {}", e);
1075+
});
1076+
}
10411077
}
10421078

10431079
fn recalculate_layout_areas(&mut self) {
@@ -1330,7 +1366,10 @@ impl App {
13301366
fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> {
13311367
if let Focus::MultipleOutput(pane_idx) = self.focus {
13321368
let terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
1333-
terminal_pane_data.handle_key_event(key)
1369+
if let Some(action) = terminal_pane_data.handle_key_event(key)? {
1370+
self.dispatch_action(action);
1371+
}
1372+
Ok(())
13341373
} else {
13351374
Ok(())
13361375
}
@@ -1423,15 +1462,17 @@ impl App {
14231462
parser_and_writer: External<(ParserArc, WriterArc)>,
14241463
) {
14251464
// Access the contents of the External
1426-
let parser_and_writer_clone = parser_and_writer.clone();
1427-
let (parser, writer) = &parser_and_writer_clone;
14281465
let pty = Arc::new(
1429-
PtyInstance::new(task_id.to_string(), parser.clone(), writer.clone())
1430-
.map_err(|e| napi::Error::from_reason(format!("Failed to create PTY: {}", e)))
1431-
.unwrap(),
1466+
PtyInstance::new(
1467+
task_id.to_string(),
1468+
parser_and_writer.0.clone(),
1469+
parser_and_writer.1.clone(),
1470+
)
1471+
.map_err(|e| napi::Error::from_reason(format!("Failed to create PTY: {}", e)))
1472+
.unwrap(),
14321473
);
14331474

1434-
self.pty_instances.insert(task_id.to_string(), pty.clone());
1475+
self.pty_instances.insert(task_id.to_string(), pty);
14351476
}
14361477

14371478
fn create_empty_parser_and_noop_writer() -> (ParserArc, External<(ParserArc, WriterArc)>) {
@@ -1506,4 +1547,15 @@ impl App {
15061547
self.debug_state.transition(event);
15071548
}
15081549
}
1550+
1551+
pub fn set_console_messenger(&mut self, messenger: NxConsoleMessageConnection) {
1552+
self.console_messenger = Some(messenger);
1553+
if self
1554+
.console_messenger
1555+
.as_ref()
1556+
.is_some_and(|c| c.is_connected())
1557+
{
1558+
self.dispatch_action(Action::ConsoleMessagesAvailable(true));
1559+
}
1560+
}
15091561
}

packages/nx/src/native/tui/components/help_popup.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ use tokio::sync::mpsc::UnboundedSender;
1313

1414
use super::{Component, Frame};
1515
use crate::native::tui::action::Action;
16+
1617
use crate::native::tui::theme::THEME;
1718

19+
#[derive(Default)]
1820
pub struct HelpPopup {
1921
scroll_offset: usize,
2022
scrollbar_state: ScrollbarState,
2123
content_height: usize,
2224
viewport_height: usize,
2325
visible: bool,
2426
action_tx: Option<UnboundedSender<Action>>,
27+
console_available: bool,
2528
}
2629

2730
impl HelpPopup {
@@ -33,13 +36,18 @@ impl HelpPopup {
3336
viewport_height: 0,
3437
visible: false,
3538
action_tx: None,
39+
console_available: false,
3640
}
3741
}
3842

3943
pub fn set_visible(&mut self, visible: bool) {
4044
self.visible = visible;
4145
}
4246

47+
pub fn set_console_available(&mut self, available: bool) {
48+
self.console_available = available;
49+
}
50+
4351
// Ensure the scroll state is reset to avoid recalc issues
4452
pub fn handle_resize(&mut self, _width: u16, _height: u16) {
4553
self.scroll_offset = 0;
@@ -110,7 +118,7 @@ impl HelpPopup {
110118
])
111119
.split(popup_layout[1])[1];
112120

113-
let keybindings = vec![
121+
let mut keybindings = vec![
114122
// Misc
115123
("?", "Toggle this popup"),
116124
("q or <ctrl>+c", "Quit the TUI"),
@@ -149,6 +157,17 @@ impl HelpPopup {
149157
("<ctrl>+z", "Stop interacting with a continuous task"),
150158
];
151159

160+
if self.console_available {
161+
// add Copilot specific keybindings for AI assistance
162+
keybindings.extend([
163+
("", ""),
164+
(
165+
"<ctrl>+a",
166+
"Send this output to Copilot so that it can assist with any issues",
167+
),
168+
]);
169+
}
170+
152171
let mut content: Vec<Line> = vec![
153172
// Welcome text
154173
Line::from(vec![
@@ -357,8 +376,14 @@ impl Component for HelpPopup {
357376
}
358377

359378
fn update(&mut self, action: Action) -> Result<Option<Action>> {
360-
if let Action::Resize(w, h) = action {
361-
self.handle_resize(w, h);
379+
match action {
380+
Action::Resize(w, h) => {
381+
self.handle_resize(w, h);
382+
}
383+
Action::ConsoleMessagesAvailable(available) => {
384+
self.set_console_available(available);
385+
}
386+
_ => {}
362387
}
363388
Ok(None)
364389
}

packages/nx/src/native/tui/components/tasks_list.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ratatui::{
77
text::{Line, Span},
88
widgets::{Block, Cell, Paragraph, Row, Table},
99
};
10+
use serde::{Deserialize, Serialize};
1011
use std::{
1112
any::Any,
1213
sync::{Arc, Mutex},
@@ -52,7 +53,7 @@ pub struct TaskItem {
5253
cache_status: String,
5354
// Public to aid with sorting utility and testing
5455
pub status: TaskStatus,
55-
terminal_output: String,
56+
pub terminal_output: String,
5657
pub continuous: bool,
5758
start_time: Option<i64>,
5859
// Public to aid with sorting utility and testing
@@ -115,7 +116,7 @@ impl TaskItem {
115116
}
116117

117118
#[napi]
118-
#[derive(Debug, PartialEq, Eq)]
119+
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
119120
pub enum TaskStatus {
120121
// Explicit statuses that can come from the task runner
121122
Success,

0 commit comments

Comments
 (0)