Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog";
import { ScrollArea } from "@liveagent/ui/components/ui/scroll-area";
import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/WorkspaceOverlayHost";
import { LocaleContext, t as translate } from "@liveagent/ui/i18n/index";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import { normalizeLogicalLineEndings } from "@liveagent/ui/lib/chat/composerText";
import { openChatFileLink } from "@liveagent/ui/lib/chat/openChatFileLink";
import { queuedChatTurnHasContent } from "@liveagent/ui/lib/chat/queuedChatTurn";
Expand Down Expand Up @@ -52,7 +53,6 @@ import {
} from "react";
import { ChevronDown, PanelRightClose, PanelRightOpen, Terminal } from "@/components/icons";
import { registerAskUserQuestionAnswerHandler } from "@/lib/chat/askUserQuestionBridge";
import type { ChatFileLink } from "@/lib/chat/chatFileLinks";
import type { ChatHistorySummary } from "@/lib/chat/chatHistory";
import { buildModelOptions } from "@/lib/chat/chatPageHelpers";
import type { HistoryMessageRef } from "@/lib/chat/conversationState";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@liveagent/ui/components/chat/TranscriptMessageActions";
import { Markdown } from "@liveagent/ui/components/Markdown";
import { useLocale } from "@liveagent/ui/i18n/LocaleContext";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import {
getUploadedImagePreviewCacheKey,
loadUploadedImagePreview,
Expand Down Expand Up @@ -47,7 +48,6 @@ import {
useRef,
useState,
} from "react";
import type { ChatFileLink } from "@/lib/chat/chatFileLinks";
import { normalizeLiveToolStatus, VIBING_STATUS } from "@/lib/chat/chatPageHelpers";
import type { HistoryMessageRef } from "@/lib/chat/conversationState";
import { getRoundText } from "@/lib/chat/uiMessages";
Expand Down
6 changes: 5 additions & 1 deletion crates/agent-gateway/web/test/chat-file-links.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ const {
decodeChatFileLinkPayload,
encodeChatFileLink,
parseChatFileLink,
} = loader.loadModule("src/lib/chat/chatFileLinks.ts");
} = loader.loadModule("@liveagent/ui/lib/chat/chatFileLinks.ts");

const validCases = [
["C:/work/src/a.ts", { path: "C:/work/src/a.ts", source: "absolute" }],
[String.raw`C:\work\src\a.ts`, { path: "C:/work/src/a.ts", source: "absolute" }],
[String.raw`C:\\project\\file.ts`, { path: "C:/project/file.ts", source: "absolute" }],
["D:/other/a.ts", { path: "D:/other/a.ts", source: "absolute" }],
["/D:/workspace/release/a.zip", { path: "D:/workspace/release/a.zip", source: "absolute" }],
["/d:/workspace/release/a.zip", { path: "d:/workspace/release/a.zip", source: "absolute" }],
["~/release/a.zip", { path: "~/release/a.zip", source: "absolute" }],
["~/work/a.ts:12", { path: "~/work/a.ts", line: 12, source: "absolute" }],
["C:/work/src/a.ts:12", { path: "C:/work/src/a.ts", line: 12, source: "absolute" }],
[
"C:/work/src/a.ts:12:4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ const EXECUTABLE_EXTENSIONS: &[&str] = &[
"xpi",
];

// Build artifacts the chat frequently links to (release archives, exports).
// They are neither editable text nor previewable, so reveal them in the host
// file manager instead of failing closed.
const ARCHIVE_EXTENSIONS: &[&str] = &[
"7z", "bz2", "cab", "gz", "iso", "lz4", "lzma", "rar", "tar", "tbz2", "tgz", "txz", "xz",
"zip", "zst",
];

#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatFileLinkErrorCode {
Expand Down Expand Up @@ -382,7 +390,14 @@ fn build_chat_file_link_plan(
));
}

let raw_target = PathBuf::from(path.trim());
// Home-anchored link paths ("~/release/a.zip") expand against the host
// home directory, mirroring the frontend's absolute classification.
// Relative paths keep their literal form so a workspace entry named "~"
// still joins against the conversation workdir.
let raw_target = match source {
"absolute" | "file-url" => expand_tilde_path(path.trim()),
_ => PathBuf::from(path.trim()),
};
let candidate = match source {
"relative" if !raw_target.is_absolute() => conversation_workdir.join(raw_target),
"absolute" | "file-url" if raw_target.is_absolute() => raw_target,
Expand Down Expand Up @@ -475,7 +490,7 @@ fn build_chat_file_link_plan(
"editor"
} else if has_extension(&target, PREVIEW_EXTENSIONS) {
"preview"
} else if executable {
} else if executable || has_extension(&target, ARCHIVE_EXTENSIONS) {
"revealed"
} else if is_probably_text(&target) {
"editor"
Expand Down Expand Up @@ -741,6 +756,60 @@ mod tests {
fs::remove_dir_all(root).expect("remove temp workspace");
}

#[test]
fn archive_files_reveal_in_the_file_manager_instead_of_failing_closed() {
let root = temp_workspace();
for name in ["release-1.0.4.zip", "backup.tar", "export.7z", "logs.tgz"] {
fs::write(root.join(name), [0x50_u8, 0x4b, 0x03, 0x04]).expect("write archive");
let planned = plan(&root, name, "relative");
assert_eq!(planned.response.action, "revealed", "{name}");
assert_eq!(planned.system_mode, Some("reveal"), "{name}");
}
fs::remove_dir_all(root).expect("remove temp workspace");
}

#[test]
fn home_anchored_absolute_links_expand_against_the_host_home() {
let Some(home) = dirs::home_dir() else {
return;
};
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let home_dir_name = format!("liveagent-chat-file-links-home-{suffix}");
let home_target_dir = home.join(&home_dir_name);
if fs::create_dir_all(&home_target_dir).is_err() {
return;
}
fs::write(home_target_dir.join("notes.md"), "# home\n").expect("write home file");
let root = temp_workspace();

let planned = build_chat_file_link_plan(
"conversation-test",
&root.to_string_lossy(),
&format!("~/{home_dir_name}/notes.md"),
"absolute",
None,
None,
None,
false,
)
.expect("home-anchored link must resolve");
assert_eq!(planned.response.action, "editor");
assert!(planned.response.outside_workspace);

// A workspace entry literally named "~" keeps joining relatively.
fs::create_dir(root.join("~")).expect("create literal tilde dir");
fs::write(root.join("~/inner.md"), "# inner\n").expect("write literal tilde file");
let relative = plan(&root, "~/inner.md", "relative");
assert_eq!(relative.response.action, "editor");
assert!(!relative.response.outside_workspace);

fs::remove_dir_all(root).expect("remove temp workspace");
fs::remove_dir_all(home_target_dir).expect("remove home dir");
}

#[test]
fn workspace_directories_use_file_tree_first_and_support_a_safe_manager_fallback() {
let root = temp_workspace();
Expand Down
1 change: 0 additions & 1 deletion crates/agent-gui/src/lib/chat/chatFileLinks.ts

This file was deleted.

201 changes: 0 additions & 201 deletions crates/agent-gui/src/lib/chat/messages/chatFileLinks.ts

This file was deleted.

2 changes: 1 addition & 1 deletion crates/agent-gui/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog";
import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/WorkspaceOverlayHost";
import { useLocale } from "@liveagent/ui/i18n/index";
import { getAutomationState, useAutomation } from "@liveagent/ui/lib/automation/index";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import { openChatFileLink } from "@liveagent/ui/lib/chat/openChatFileLink";
import { selectLatestTaskProgress } from "@liveagent/ui/lib/chat/taskProgress";
import type { ScrollFollowHandle } from "@liveagent/ui/lib/chat-scroll/useScrollFollow";
Expand Down Expand Up @@ -58,7 +59,6 @@ import { WorkspaceCloneTaskOverlayAdapter } from "../agent-ui-adapters/workspace
import { PanelRightClose, PanelRightOpen } from "../components/icons";
import { MacOsTitleBarToggle } from "../components/MacOsTitleBarSpacer";
import type { AppUpdateController } from "../lib/appUpdates";
import type { ChatFileLink } from "../lib/chat/chatFileLinks";
import type { CompactionStatus } from "../lib/chat/compaction/types";
import {
buildRequestContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
RoundBlockContent,
} from "@liveagent/ui/components/chat/assistant-bubble/RoundContent";
import { UsagePanel } from "@liveagent/ui/components/chat/UsagePanel";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import { memo, type ReactNode } from "react";
import type { ChatFileLink } from "../../../lib/chat/chatFileLinks";
import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore";
import { VIBING_STATUS } from "../../../lib/chat/page/chatPageHelpers";
import type { AssistantUnitRow } from "../transcript/rowModel";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import { memo } from "react";

import type { ChatFileLink } from "../../../lib/chat/chatFileLinks";
import type { HistoryMessageRef } from "../../../lib/chat/conversation/conversationState";
import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore";
import type { PendingUploadedFile } from "../../../lib/chat/messages/uploadedFiles";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangedFilesCard } from "@liveagent/ui/components/chat/ChangedFilesCard";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import { memo, useMemo } from "react";
import type { ChatFileLink } from "../../../lib/chat/chatFileLinks";
import type { HistoryMessageRef } from "../../../lib/chat/conversation/conversationState";
import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore";
import { collectChangedFiles } from "../../../lib/chat/messages/changedFiles";
Expand Down
Loading
Loading