feat(workspace): add tilde expansion for workspace paths

Add support for expanding tilde (~) to the user's home directory in workspace
path operations. This allows users to use shorthand paths like '~/Documents'
instead of full absolute paths when configuring their workspace.

- Added expandTilde() utility function with cross-platform support
- Updated validatePath to return expanded path for UI consumption
- Applied expansion to listDirectory and readFile operations
- Modified UI to store and display expanded absolute paths
This commit is contained in:
Jamie Taylor
2026-02-01 18:25:07 +00:00
parent 87a6e71aa9
commit 4ef95e6df1
3 changed files with 43 additions and 9 deletions
+11 -5
View File
@@ -16,6 +16,7 @@ import {
readFile,
pathExists,
getDefaultWorkspacePath,
expandTilde,
type DirectoryEntry,
type FileContent,
} from '~/lib/workspace-fs'
@@ -229,13 +230,14 @@ const workspaceRouter = router({
// Validate workspace path exists
validatePath: publicProcedure
.input(z.object({ path: z.string() }))
.query(async ({ input }): Promise<{ valid: boolean; error?: string }> => {
.query(async ({ input }): Promise<{ valid: boolean; error?: string; expandedPath?: string }> => {
try {
const exists = await pathExists(input.path)
const expandedPath = expandTilde(input.path)
const exists = await pathExists(expandedPath)
if (!exists) {
return { valid: false, error: 'Path does not exist' }
}
return { valid: true }
return { valid: true, expandedPath }
} catch (error) {
return {
valid: false,
@@ -254,7 +256,9 @@ const workspaceRouter = router({
.input(z.object({ workspaceRoot: z.string(), path: z.string() }))
.query(async ({ input }): Promise<{ entries: DirectoryEntry[]; error?: string }> => {
try {
const entries = await listDirectory(input.workspaceRoot, input.path)
const expandedRoot = expandTilde(input.workspaceRoot)
const expandedPath = expandTilde(input.path)
const entries = await listDirectory(expandedRoot, expandedPath)
return { entries }
} catch (error) {
return {
@@ -269,7 +273,9 @@ const workspaceRouter = router({
.input(z.object({ workspaceRoot: z.string(), path: z.string() }))
.query(async ({ input }): Promise<FileContent & { error?: string }> => {
try {
const result = await readFile(input.workspaceRoot, input.path)
const expandedRoot = expandTilde(input.workspaceRoot)
const expandedPath = expandTilde(input.path)
const result = await readFile(expandedRoot, expandedPath)
return result
} catch (error) {
return {
+27 -1
View File
@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'
import os from 'os'
import path from 'path'
/**
@@ -158,7 +159,7 @@ export async function pathExists(targetPath: string): Promise<boolean> {
* Returns the user's home directory + .openclaw/workspace
*/
export function getDefaultWorkspacePath(): string {
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'
const homeDir = os.homedir()
return path.join(homeDir, '.openclaw', 'workspace')
}
@@ -170,6 +171,31 @@ export function isMarkdownFile(filename: string): boolean {
return ext === '.md' || ext === '.markdown'
}
/**
* Expands tilde (~) to the user's home directory on Unix-based systems
* Handles both "~/" prefix and standalone "~" path
*/
export function expandTilde(inputPath: string): string {
// Only expand if path starts with ~
if (!inputPath.startsWith('~')) {
return inputPath
}
// Get home directory using Node.js built-in (handles cross-platform)
// Returns /root in containerized environments if HOME is not set
const homeDir = os.homedir()
// Handle "~/" prefix or standalone "~"
if (inputPath === '~' || inputPath.startsWith('~/')) {
return path.join(homeDir, inputPath.slice(1))
}
// Path starts with ~ but not followed by / (e.g., ~username)
// This is a valid Unix path referring to another user's home
// Return as-is and let the system handle it
return inputPath
}
/**
* Checks if a file is viewable as text
*/
+5 -3
View File
@@ -128,11 +128,13 @@ function WorkspacePage() {
path: pathToValidate,
})
if (result.valid) {
setWorkspacePath(pathToValidate)
if (result.valid && result.expandedPath) {
// Use the expanded path (e.g., ~/Documents -> /home/user/Documents)
setWorkspacePath(result.expandedPath)
setWorkspacePathInput(result.expandedPath)
setPathValid(true)
// Persist to localStorage
localStorage.setItem('crabcrawl:workspacePath', pathToValidate)
localStorage.setItem('crabcrawl:workspacePath', result.expandedPath)
// Clear cache when path changes
setPathCache(new Map())
setSelectedPath(null)