mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
feat(terminal): give the composer a chat-box layout with attachments and / @ menus
- A rounded, tinted frame whose hairline darkens on focus and on a file drag, a toolbar row with an attach button and a round send button that fills in once there is something to send. The key hints move into the send button's tooltip; the placeholder names the agent and what / and @ do. - Files become chips instead of words in the text: the attach button, a drop on the box, a pasted screenshot or copied file, and remote uploads all go through one paste_paths route that attaches while the box has focus. Backspace at the start of an empty caret removes the last chip. On send the paths follow the text as the shell words a drop would have typed. - `/` at the start of the message opens the agent's built-in commands (Claude Code, Codex, Gemini) plus Claude's custom commands from .claude/commands; `@` at the start of any word opens files from the pane's project, reusing the quick-open walk so it works on remote hosts too, spelled relative to the pane's directory. Up/Down move, Enter/Tab pick, Esc dismisses. - Shift+Tab is passed through to the agent to cycle its permission mode. - Enter with nothing to send no longer sends a bare Enter. No model picker, mode label or context meter: tty7 has no source for them that would stay true, and /model is one keystroke away in the menu.
This commit is contained in:
+880
-107
File diff suppressed because it is too large
Load Diff
+25
-18
@@ -1028,7 +1028,7 @@ fn clipboard_paths(item: &ClipboardItem) -> Vec<std::path::PathBuf> {
|
||||
|
||||
/// Paths as one line of shell words, trailing space included so whatever is
|
||||
/// typed next starts a word of its own.
|
||||
fn pasted_paths_text(paths: &[String], shell: Option<&str>) -> String {
|
||||
pub(super) fn pasted_paths_text(paths: &[String], shell: Option<&str>) -> String {
|
||||
let words: Vec<String> = paths.iter().map(|p| quote_for_shell(p, shell)).collect();
|
||||
format!("{} ", words.join(" "))
|
||||
}
|
||||
@@ -2362,7 +2362,7 @@ impl TerminalView {
|
||||
/// answers from the platform — PowerShell on Windows, POSIX elsewhere.
|
||||
/// The one pane that guess is wrong for is a cmd.exe pane that has not
|
||||
/// reported in yet.
|
||||
fn shell_program(&self) -> Option<String> {
|
||||
pub(super) fn shell_program(&self) -> Option<String> {
|
||||
self.shell_spec.as_ref().map(|s| s.program.clone())
|
||||
}
|
||||
|
||||
@@ -3371,9 +3371,6 @@ impl TerminalView {
|
||||
if !self.accepts_input(cx) {
|
||||
return;
|
||||
}
|
||||
let Some(text) = self.composer_takes_paste(text, cx) else {
|
||||
return;
|
||||
};
|
||||
// Same reason as `commit_text`: what is pasted lands on the prompt, so
|
||||
// the prompt is what has to be on screen. Neither branch below moved
|
||||
// the viewport, and a paste is a bigger change than a keystroke to
|
||||
@@ -3636,29 +3633,42 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_files(&mut self, paths: &ExternalPaths, cx: &mut Context<Self>) {
|
||||
pub(super) fn drop_files(&mut self, paths: &ExternalPaths, cx: &mut Context<Self>) {
|
||||
self.paste_local_paths(paths.paths().to_vec(), cx);
|
||||
}
|
||||
|
||||
/// A row dragged out of a remote Files tree names a file on that remote,
|
||||
/// not here: there is nothing to upload, and the path is pasted as the
|
||||
/// tree spells it.
|
||||
fn drop_remote_path(
|
||||
pub(super) fn drop_remote_path(
|
||||
&mut self,
|
||||
drag: &crate::ui::file_tree::RemotePathDrag,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let text = pasted_paths_text(
|
||||
&[drag.path.to_string_lossy().into_owned()],
|
||||
self.shell_program().as_deref(),
|
||||
);
|
||||
self.paste_paths(vec![drag.path.to_string_lossy().into_owned()], cx);
|
||||
}
|
||||
|
||||
/// Paste paths already spelled the way the pane's host reads them: into
|
||||
/// the composer as attachments when it has the keyboard, and as shell
|
||||
/// words onto the line otherwise. Every route that turns files into
|
||||
/// something to paste — a drop, a copied file, a screenshot, an upload to
|
||||
/// a remote pane — ends here.
|
||||
fn paste_paths(&mut self, spelled: Vec<String>, cx: &mut Context<Self>) {
|
||||
let Some(spelled) = self.composer_takes_paths(spelled, cx) else {
|
||||
return;
|
||||
};
|
||||
let text = pasted_paths_text(&spelled, self.shell_program().as_deref());
|
||||
self.paste(text, cx);
|
||||
}
|
||||
|
||||
/// Paste files that live on this machine so the pane's program can open
|
||||
/// them: uploaded first when the pane runs on an SSH host, renamed for a
|
||||
/// WSL pane, and pasted as they are everywhere else.
|
||||
fn paste_local_paths(&mut self, paths: Vec<std::path::PathBuf>, cx: &mut Context<Self>) {
|
||||
pub(super) fn paste_local_paths(
|
||||
&mut self,
|
||||
paths: Vec<std::path::PathBuf>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -3679,8 +3689,7 @@ impl TerminalView {
|
||||
.iter()
|
||||
.map(|p| local_path_for_pane(&p.to_string_lossy(), shares_localhost))
|
||||
.collect();
|
||||
let text = pasted_paths_text(&spelled, self.shell_program().as_deref());
|
||||
self.paste(text, cx);
|
||||
self.paste_paths(spelled, cx);
|
||||
}
|
||||
|
||||
fn paste_clipboard_image(&mut self, img: &gpui::Image, cx: &mut Context<Self>) {
|
||||
@@ -3788,8 +3797,7 @@ impl TerminalView {
|
||||
})
|
||||
.collect();
|
||||
let pasted = this.update_in(cx, |view, window, cx| {
|
||||
let text = pasted_paths_text(&spelled, view.shell_program().as_deref());
|
||||
view.paste(text, cx);
|
||||
view.paste_paths(spelled, cx);
|
||||
for u in &uploads {
|
||||
if let Err(reason) = &u.started {
|
||||
view.warn_paste_upload_failed(&u.local, &host, reason, window, cx);
|
||||
@@ -3826,8 +3834,7 @@ impl TerminalView {
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
let _ = this.update_in(cx, |view, window, cx| {
|
||||
let text = pasted_paths_text(&spelled, view.shell_program().as_deref());
|
||||
view.paste(text, cx);
|
||||
view.paste_paths(spelled, cx);
|
||||
if let Some(first) = sources.first() {
|
||||
view.warn_paste_upload_failed(first, host, reason, window, cx);
|
||||
}
|
||||
|
||||
+8
-4
@@ -1855,10 +1855,14 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::CmdClearScrollback => "Clear Scrollback",
|
||||
L10nKey::CmdFindInTerminal => "Find in Terminal…",
|
||||
L10nKey::CmdToggleComposer => "Toggle Message Composer",
|
||||
L10nKey::ComposerPlaceholder => "Message {agent}…",
|
||||
L10nKey::ComposerKeys => {
|
||||
"Enter to send · Shift+Enter for a new line · Esc to go back to the terminal"
|
||||
}
|
||||
L10nKey::ComposerPlaceholder => "Message {agent} · @ files / commands",
|
||||
L10nKey::ComposerPlaceholderFiles => "Message {agent} · @ files",
|
||||
L10nKey::ComposerSendTip => "Send (Enter) · Shift+Enter for a new line",
|
||||
L10nKey::ComposerAttach => "Attach files or images",
|
||||
L10nKey::ComposerMenuCommands => "Commands",
|
||||
L10nKey::ComposerMenuFiles => "Files",
|
||||
L10nKey::ComposerCmdProject => "Project command",
|
||||
L10nKey::ComposerCmdUser => "User command",
|
||||
L10nKey::ComposerAgentAsking => {
|
||||
"{agent} is asking something. Answer it in the terminal, then send."
|
||||
}
|
||||
|
||||
+8
-2
@@ -1908,8 +1908,14 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CmdClearScrollback => "スクロールバックをクリア",
|
||||
L10nKey::CmdFindInTerminal => "ターミナル内を検索…",
|
||||
L10nKey::CmdToggleComposer => "メッセージ入力欄の切り替え",
|
||||
L10nKey::ComposerPlaceholder => "{agent} にメッセージ…",
|
||||
L10nKey::ComposerKeys => "Enter で送信 · Shift+Enter で改行 · Esc でターミナルに戻る",
|
||||
L10nKey::ComposerPlaceholder => "{agent} にメッセージ · @ ファイル / コマンド",
|
||||
L10nKey::ComposerPlaceholderFiles => "{agent} にメッセージ · @ ファイル",
|
||||
L10nKey::ComposerSendTip => "送信(Enter) · Shift+Enter で改行",
|
||||
L10nKey::ComposerAttach => "ファイルや画像を添付",
|
||||
L10nKey::ComposerMenuCommands => "コマンド",
|
||||
L10nKey::ComposerMenuFiles => "ファイル",
|
||||
L10nKey::ComposerCmdProject => "プロジェクトのコマンド",
|
||||
L10nKey::ComposerCmdUser => "ユーザーのコマンド",
|
||||
L10nKey::ComposerAgentAsking => {
|
||||
"{agent} が質問しています。ターミナルで答えてから送信してください。"
|
||||
}
|
||||
|
||||
+7
-1
@@ -1569,7 +1569,13 @@ l10n_keys! {
|
||||
CmdFindInTerminal,
|
||||
CmdToggleComposer,
|
||||
ComposerPlaceholder,
|
||||
ComposerKeys,
|
||||
ComposerPlaceholderFiles,
|
||||
ComposerSendTip,
|
||||
ComposerAttach,
|
||||
ComposerMenuCommands,
|
||||
ComposerMenuFiles,
|
||||
ComposerCmdProject,
|
||||
ComposerCmdUser,
|
||||
ComposerAgentAsking,
|
||||
CmdFindNext,
|
||||
CmdFindPrevious,
|
||||
|
||||
+8
-2
@@ -1720,8 +1720,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CmdGitToggleGraph => "Git:显示 / 隐藏提交历史",
|
||||
L10nKey::CmdFindInTerminal => "在终端中查找…",
|
||||
L10nKey::CmdToggleComposer => "切换消息输入框",
|
||||
L10nKey::ComposerPlaceholder => "给 {agent} 发消息…",
|
||||
L10nKey::ComposerKeys => "Enter 发送 · Shift+Enter 换行 · Esc 回到终端",
|
||||
L10nKey::ComposerPlaceholder => "给 {agent} 发消息 · @ 文件 / 命令",
|
||||
L10nKey::ComposerPlaceholderFiles => "给 {agent} 发消息 · @ 文件",
|
||||
L10nKey::ComposerSendTip => "发送(Enter) · Shift+Enter 换行",
|
||||
L10nKey::ComposerAttach => "附加文件或图片",
|
||||
L10nKey::ComposerMenuCommands => "命令",
|
||||
L10nKey::ComposerMenuFiles => "文件",
|
||||
L10nKey::ComposerCmdProject => "项目命令",
|
||||
L10nKey::ComposerCmdUser => "用户命令",
|
||||
L10nKey::ComposerAgentAsking => "{agent} 正在等你回答,请先在终端里回答再发送。",
|
||||
L10nKey::CmdFindNext => "查找下一个",
|
||||
L10nKey::CmdFindPrevious => "查找上一个",
|
||||
|
||||
@@ -365,7 +365,7 @@ pub(crate) struct FileIndexStore {
|
||||
|
||||
/// Walks the project the panes in `cwds` are in, on the host that has it.
|
||||
/// Runs off the UI thread.
|
||||
fn walk(h: &dyn Host, cwds: &[PathBuf], home: Option<&Path>) -> FileList {
|
||||
pub(crate) fn walk(h: &dyn Host, cwds: &[PathBuf], home: Option<&Path>) -> FileList {
|
||||
let resolved = cwds
|
||||
.iter()
|
||||
.map(|cwd| {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! - [`view`]: the modal — the tab row, the list, the theme picker.
|
||||
|
||||
mod command;
|
||||
mod files;
|
||||
pub(crate) mod files;
|
||||
mod score;
|
||||
mod sources;
|
||||
mod view;
|
||||
|
||||
Reference in New Issue
Block a user