FileManagerView rewritten as a native DS browser on the per-instance file bridge: instance selector, breadcrumb nav, dir-first listing (name/size/modified), folder drill-down, inline file editor (read/save), toolbar (new folder/file/refresh), per-row rename + delete-confirm. New files store wraps the /instances/:id/files* endpoints. VueFinder import + RemoteDriver fully removed — no more retired-protocol /api/files. Honest empty (no instance -> Server page) + error (retry) states, never the global error boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import { useApi } from '@/composables/useApi'
|
|
import { useInstancesStore } from '@/stores/instances'
|
|
|
|
export interface FileEntry {
|
|
name: string
|
|
path: string
|
|
is_dir: boolean
|
|
size: number
|
|
modified: string
|
|
}
|
|
|
|
/**
|
|
* Per-instance file browser store.
|
|
* All operations target `/api/instances/{id}/...` — jailed to instance root.
|
|
* Guard: if no current instance, list() sets error and bails out early.
|
|
*/
|
|
export const useFilesStore = defineStore('files', () => {
|
|
const api = useApi()
|
|
|
|
const cwd = ref<string>('')
|
|
const entries = ref<FileEntry[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
/** Join two relative path segments with a single forward slash. */
|
|
function joinPath(base: string, name: string): string {
|
|
if (!base) return name
|
|
return `${base}/${name}`
|
|
}
|
|
|
|
function currentId(): string | null {
|
|
// Retrieve fresh from the store each call — avoids stale closure.
|
|
return useInstancesStore().currentId
|
|
}
|
|
|
|
/** List a directory. Sets cwd + entries. Does NOT throw — sets error. */
|
|
async function list(path: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) {
|
|
error.value = 'No instance — connect the host agent'
|
|
entries.value = []
|
|
return
|
|
}
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const data = await api.get<{ entries: FileEntry[] }>(
|
|
`/instances/${id}/files?path=${encodeURIComponent(path)}`,
|
|
)
|
|
cwd.value = path
|
|
entries.value = data.entries ?? []
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to load directory'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/** Read a text file. Returns content string. Throws on error (binary/too big/not found). */
|
|
async function readFile(path: string): Promise<string> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
const data = await api.get<{ content: string }>(
|
|
`/instances/${id}/file?path=${encodeURIComponent(path)}`,
|
|
)
|
|
return data.content ?? ''
|
|
}
|
|
|
|
/** Write / overwrite a text file. Throws on error. */
|
|
async function writeFile(path: string, content: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.put(`/instances/${id}/file`, { path, content })
|
|
}
|
|
|
|
/** Delete a file or directory (recursive). Throws on error. */
|
|
async function del(path: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/delete`, { path })
|
|
}
|
|
|
|
/** Rename within the same parent. `name` is the bare new filename. Throws on error. */
|
|
async function rename(path: string, name: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/rename`, { path, name })
|
|
}
|
|
|
|
/** Create a directory (and all missing ancestors). Throws on error. */
|
|
async function mkdir(path: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/mkdir`, { path })
|
|
}
|
|
|
|
/** Create an empty file. Throws on error. */
|
|
async function mkfile(path: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/mkfile`, { path })
|
|
}
|
|
|
|
/** Move a file or directory. Both paths are relative to the instance root. Throws on error. */
|
|
async function move(path: string, dest: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/move`, { path, dest })
|
|
}
|
|
|
|
/** Copy a file or directory. Both paths are relative to the instance root. Throws on error. */
|
|
async function copy(path: string, dest: string): Promise<void> {
|
|
const id = currentId()
|
|
if (!id) throw new Error('No instance selected')
|
|
await api.post(`/instances/${id}/files/copy`, { path, dest })
|
|
}
|
|
|
|
return {
|
|
cwd,
|
|
entries,
|
|
loading,
|
|
error,
|
|
joinPath,
|
|
list,
|
|
readFile,
|
|
writeFile,
|
|
del,
|
|
rename,
|
|
mkdir,
|
|
mkfile,
|
|
move,
|
|
copy,
|
|
}
|
|
})
|