Skip to content
Open
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
37 changes: 37 additions & 0 deletions src/core/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { exec } from "child_process"
import { promisify } from "util"

const execAsync = promisify(exec)

/**
* Read text from system clipboard.
* Attempts multiple clipboard tools based on platform.
* Returns null if no clipboard tool is available.
*/
export async function readClipboard(): Promise<string | null> {
const platform = process.platform

const commands: string[] = []

if (platform === "darwin") {
commands.push("pbpaste")
} else if (platform === "linux") {
commands.push("xclip -selection clipboard -o")
commands.push("xsel --clipboard --output")
commands.push("powershell.exe -Command Get-Clipboard")
commands.push("pwsh.exe -Command Get-Clipboard")
} else if (platform === "win32") {
commands.push("powershell.exe -Command Get-Clipboard")
}

for (const cmd of commands) {
try {
const { stdout } = await execAsync(cmd, { timeout: 2000 })
if (stdout) return stdout
} catch {
continue
}
}

return null
}
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface AppConfig {
autoHibernateMinutes?: number // 0 = disabled, default 0
autoHibernatePrompted?: boolean // true = user has seen the prompt
lastRemoteSession?: LastRemoteSession // Last used remote session values
copyClaudeDir?: boolean // true = copy .claude dir to worktree (default false)
}

const CONFIG_DIR = path.join(os.homedir(), ".agent-view")
Expand Down
77 changes: 77 additions & 0 deletions src/core/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { readdirSync, statSync, existsSync } from "fs"
import path from "path"
import os from "os"

/**
* List subdirectories of a given directory that match a prefix.
* Returns absolute paths for matching directories.
*/
export function listSubdirectories(dirPath: string, prefix?: string): string[] {
const expanded = dirPath.startsWith("~")
? path.join(os.homedir(), dirPath.slice(1))
: dirPath

if (!expanded || !existsSync(expanded)) return []

try {
const entries = readdirSync(expanded, { withFileTypes: true })
return entries
.filter(e => e.isDirectory() && !e.name.startsWith("."))
.filter(e => !prefix || e.name.startsWith(prefix))
.map(e => path.join(expanded, e.name))
} catch {
return []
}
}

/**
* Given a partial path, resolve the parent directory and prefix
* to match against, then return matching subdirectory completions.
*/
export function resolvePathCompletion(partialPath: string): {
parentDir: string
prefix: string
completions: string[]
} {
let expanded = partialPath.startsWith("~")
? path.join(os.homedir(), partialPath.slice(1))
: partialPath

if (!expanded) expanded = process.cwd()

let parentDir: string
let prefix: string

if (expanded.endsWith("/") || expanded.endsWith(path.sep)) {
parentDir = expanded
prefix = ""
} else {
parentDir = path.dirname(expanded)
prefix = path.basename(expanded)
}

if (!existsSync(parentDir) && prefix) {
const grandparent = path.dirname(parentDir)
if (existsSync(grandparent)) {
const parentPrefix = path.basename(parentDir)
const completions = listSubdirectories(grandparent, parentPrefix)
return { parentDir: grandparent, prefix: parentPrefix, completions }
}
return { parentDir, prefix, completions: [] }
}

const completions = listSubdirectories(parentDir, prefix)
return { parentDir, prefix, completions }
}

/**
* Check if a directory is a git repository (synchronous, for autocomplete).
*/
export function isDirGitRepoSync(dirPath: string): boolean {
try {
statSync(path.join(dirPath, ".git"))
return true
} catch {
return false
}
}
13 changes: 13 additions & 0 deletions src/core/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { exec } from "child_process"
import { promisify } from "util"
import * as path from "path"
import * as os from "os"
import { existsSync } from "fs"
import { cp } from "fs/promises"

const execAsync = promisify(exec)

Expand Down Expand Up @@ -338,3 +340,14 @@ export async function pruneWorktrees(repoDir: string): Promise<void> {
throw new Error(`failed to prune worktrees: ${output}`)
}
}

/**
* Copy the .claude directory from the repo root into a worktree.
* No-op if the source directory does not exist.
*/
export async function copyClaudeDir(repoRoot: string, worktreePath: string): Promise<void> {
const src = path.join(repoRoot, ".claude")
const dest = path.join(worktreePath, ".claude")
if (!existsSync(src)) return
await cp(src, dest, { recursive: true })
}
8 changes: 4 additions & 4 deletions src/core/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class SSHRunner {

// Exit alternate screen buffer before attaching
process.stdout.write("\x1b[?1049l")
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?25h")

const sshArgs = [
Expand All @@ -159,7 +159,7 @@ export class SSHRunner {

child.on("exit", () => {
// Clear screen and re-enter alternate buffer for TUI
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?1049h")
process.stdout.write("\x1b]0;Agent View\x07")
})
Expand All @@ -175,7 +175,7 @@ export class SSHRunner {

// Exit alternate screen buffer
process.stdout.write("\x1b[?1049l")
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?25h")

const sshArgs = [
Expand Down Expand Up @@ -207,7 +207,7 @@ export class SSHRunner {
}

// Clear screen and re-enter alternate buffer for TUI
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?1049h")
process.stdout.write("\x1b]0;Agent View\x07")

Expand Down
10 changes: 7 additions & 3 deletions src/core/tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
# Reserved keybinds (avoid overriding in tmux-user.conf):
# Ctrl+Q - detach
# Ctrl+D - delete session
# Ctrl+K - command palette
# Ctrl+L - session list
# Ctrl+T - toggle terminal pane
Expand All @@ -20,13 +21,16 @@

# --- Keybindings ---
bind-key -n C-q detach-client
bind-key -n C-d run-shell "touch /tmp/agent-view-delete-session" \; detach-client
bind-key -n C-k run-shell "touch /tmp/agent-view-cmd-palette" \; detach-client
bind-key -n C-t if-shell "[ $(tmux display -p '#{window_panes}') -eq 1 ]" "split-window -v" "kill-pane -t :.1"
bind-key -n C-o select-pane -t :.+

# Session switching (no prefix needed)
bind-key -n C-] switch-client -n
bind-key -n 'C-\' switch-client -p
# refresh-client after switch forces a full terminal redraw to prevent
# screen artifacts from the previous session.
bind-key -n C-] switch-client -n \; refresh-client
bind-key -n 'C-\' switch-client -p \; refresh-client
bind-key -n C-l run-shell "touch /tmp/agent-view-session-list" \; detach-client

# --- Status bar ---
Expand All @@ -36,7 +40,7 @@ set-option -g status-style "bg=#1e1e2e,fg=#cdd6f4"
set-option -g status-left "#[fg=#a6e3a1,bold] #{window_name} #[fg=#6c7086]| "
set-option -g status-left-length 30
set-option -g status-right-length 120
set-option -g status-right "#[fg=#89b4fa]Ctrl+]/\\#[fg=#6c7086] switch #[fg=#89b4fa]Ctrl+L#[fg=#6c7086] list #[fg=#89b4fa]Ctrl+T#[fg=#6c7086] term #[fg=#89b4fa]Ctrl+K#[fg=#6c7086] cmd #[fg=#89b4fa]Ctrl+Q#[fg=#6c7086] detach"
set-option -g status-right "#[fg=#89b4fa]Ctrl+]/\\#[fg=#6c7086] switch #[fg=#89b4fa]Ctrl+L#[fg=#6c7086] list #[fg=#89b4fa]Ctrl+T#[fg=#6c7086] term #[fg=#89b4fa]Ctrl+K#[fg=#6c7086] cmd #[fg=#89b4fa]Ctrl+D#[fg=#6c7086] delete #[fg=#89b4fa]Ctrl+Q#[fg=#6c7086] detach"

# --- Terminal titles ---
set-option -g set-titles on
Expand Down
24 changes: 21 additions & 3 deletions src/core/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const SESSION_PREFIX = "agentorch_"
// Signal files for UI requests from tmux keybinds
const COMMAND_PALETTE_SIGNAL = "/tmp/agent-view-cmd-palette"
const SESSION_LIST_SIGNAL = "/tmp/agent-view-session-list"
const DELETE_SESSION_SIGNAL = "/tmp/agent-view-delete-session"

// --- Isolated tmux server configuration ---
// All agent-view sessions run on a dedicated tmux socket with a custom config,
Expand Down Expand Up @@ -615,7 +616,7 @@ export async function attachWithPty(sessionName: string): Promise<void> {
}

// Clear screen before returning to TUI
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
}
})
}
Expand Down Expand Up @@ -647,6 +648,18 @@ export function wasSessionListRequested(): boolean {
return false
}

export function wasDeleteSessionRequested(): boolean {
try {
if (fs.existsSync(DELETE_SESSION_SIGNAL)) {
fs.unlinkSync(DELETE_SESSION_SIGNAL)
return true
}
} catch {
// Ignore errors
}
return false
}

/**
* Attach to a tmux session with Ctrl+Q to detach
* Keybindings and status bar are configured via the custom tmux.conf,
Expand Down Expand Up @@ -732,10 +745,15 @@ export function attachSessionSync(sessionName: string): void {
} catch {
// Ignore if doesn't exist
}
try {
fs.unlinkSync(DELETE_SESSION_SIGNAL)
} catch {
// Ignore if doesn't exist
}

// Exit alternate screen buffer (TUI uses this)
process.stdout.write("\x1b[?1049l")
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?25h")

// Attach to tmux - this blocks until user detaches (Ctrl+Q or Ctrl+B d)
Expand All @@ -748,7 +766,7 @@ export function attachSessionSync(sessionName: string): void {
})

// Clear screen and re-enter alternate buffer for TUI
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?1049h")

// Restore terminal title to "Agent View"
Expand Down
4 changes: 2 additions & 2 deletions src/core/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function performUpdateSync(): void {

// Exit alternate screen buffer
process.stdout.write("\x1b[?1049l")
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?25h")

spawnSync("bash", ["-c", "curl -fsSL https://raw.githubusercontent.com/frayo44/agent-view/main/install.sh | bash"], {
Expand All @@ -61,7 +61,7 @@ export function performUpdateSync(): void {
})

// Clear screen and re-enter alternate buffer for TUI
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
process.stdout.write("\x1b[?1049h")

// Restore terminal title
Expand Down
Loading