The activity panel, and continuing an agent chat in a terminal
The activity panel beside a chat, from /api/v2's activity routes: Context
(used share, tokens against the window, a segmented bar and a row per
category), Outputs (the files written, lines added and removed, each opening
in the viewer), Background tasks (a team's members as a tree, helpers and
commands: state, elapsed time counting on between reads, model, latest step,
what it is writing, its whole conversation read-only, Stop -- a member after
a confirmation, as it takes those under it), the team's room (who to whom,
reports marked, the words through the page's own markdown) and the plan.
The chat follows GET /chats/{chat}/activity/events while it is open: the strip
above the message box sums the agents up and opens the panel; a new version
reads the panel again while it is open, the costly context part on opening,
after a reply and at most every 15 s. The stream is reopened when the server
closes it, and more slowly while it fails. Beside the chat on a wide window
(the viewer then goes over the chat), over it below --bp-wide, a full-height
sheet at --bp-narrow. The header's Activity button, the palette and Escape
open and close it.
An agent chat with a device session (device_session_id) can be continued in a
terminal: the chat's menu and a header button show 'cd <folder> && lembas -s
<id>' to copy, with lembas the CLI as it starts here (on PATH, else the app's
own copy by its path). Shown rather than run: no terminal opens the same way
on every desktop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+1072
-40
No files matched your search
@@ -42,6 +42,37 @@ pub fn binary() -> PathBuf {
|
||||
.unwrap_or_else(|| PathBuf::from(exe_name()))
|
||||
}
|
||||
|
||||
/// How a person starts the CLI in a terminal on this computer: `lembas` where one is on PATH
|
||||
/// (installed by hand: it shares the app's sessions, the directories are the same), else the
|
||||
/// bundled one by its full path, quoted for a POSIX shell. The bundled copy is never on PATH.
|
||||
pub fn terminal_word() -> String {
|
||||
let path = std::env::var_os("PATH").unwrap_or_default();
|
||||
on_path(&path, exe_name()).map_or_else(
|
||||
|| shell_quote(&binary().to_string_lossy()),
|
||||
|_| exe_name().to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_path(path: &std::ffi::OsStr, name: &str) -> Option<PathBuf> {
|
||||
std::env::split_paths(path)
|
||||
.map(|dir| dir.join(name))
|
||||
.find(|p| p.is_file())
|
||||
}
|
||||
|
||||
/// A word for a POSIX shell: as it is when it is plainly safe, else in single quotes.
|
||||
pub fn shell_quote(word: &str) -> String {
|
||||
let plain = !word.is_empty()
|
||||
&& word.chars().all(|c| {
|
||||
c.is_ascii_alphanumeric()
|
||||
|| matches!(c, '/' | '.' | '-' | '_' | '+' | ':' | '@' | '%' | '=')
|
||||
});
|
||||
if plain {
|
||||
word.to_string()
|
||||
} else {
|
||||
format!("'{}'", word.replace('\'', "'\\''"))
|
||||
}
|
||||
}
|
||||
|
||||
fn home() -> PathBuf {
|
||||
std::env::var_os("HOME")
|
||||
.or_else(|| std::env::var_os("USERPROFILE"))
|
||||
@@ -179,6 +210,26 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_word_for_the_shell_is_quoted_only_when_it_must_be() {
|
||||
assert_eq!(
|
||||
shell_quote("/usr/lib/llembas-desktop/lembas"),
|
||||
"/usr/lib/llembas-desktop/lembas"
|
||||
);
|
||||
assert_eq!(shell_quote("/home/a b/lembas"), "'/home/a b/lembas'");
|
||||
assert_eq!(shell_quote("it's"), "'it'\\''s'");
|
||||
assert_eq!(shell_quote(""), "''");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cli_on_path_is_found_by_name() {
|
||||
let d = scratch("path");
|
||||
assert_eq!(on_path(d.as_os_str(), "lembas"), None);
|
||||
fs::write(d.join("lembas"), b"").unwrap();
|
||||
assert_eq!(on_path(d.as_os_str(), "lembas"), Some(d.join("lembas")));
|
||||
fs::remove_dir_all(&d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_reads_the_logins_the_cli_wrote() {
|
||||
let d = scratch("linked");
|
||||
|
||||
@@ -437,6 +437,13 @@ async fn model_image(handle: AppHandle, path: String) -> Result<String, ApiError
|
||||
.map_err(|e| ApiError::new(0, e.to_string()))?
|
||||
}
|
||||
|
||||
/// How the CLI is started in a terminal here (`lembas`, or the bundled one by its path): the
|
||||
/// first word of "continue in a terminal" for an agent chat on this computer.
|
||||
#[tauri::command]
|
||||
fn terminal_word() -> String {
|
||||
cli::terminal_word()
|
||||
}
|
||||
|
||||
/// A frame of a reply being written, as the page hears it.
|
||||
#[derive(Clone, Serialize)]
|
||||
struct Frame {
|
||||
@@ -1436,6 +1443,7 @@ pub fn run() {
|
||||
api,
|
||||
api_text,
|
||||
model_image,
|
||||
terminal_word,
|
||||
stream_open,
|
||||
stream_close,
|
||||
local_list,
|
||||
|
||||
Reference in new issue
Block a user