feat: Support multi-root workspaces
- Remove `base_path` from Workspace struct and APIs - Enable adding multiple root directories to a workspace - Update file tree UI to display all workspace roots - Refactor file map generation for multi-root display - Improve prompt output clipboard copy with status
This commit is contained in:
@@ -23,8 +23,7 @@ pub mut:
|
|||||||
|
|
||||||
pub fn new(args ArgsGet) !&Workspace {
|
pub fn new(args ArgsGet) !&Workspace {
|
||||||
mut obj := Workspace{
|
mut obj := Workspace{
|
||||||
name: args.name
|
name: args.name
|
||||||
base_path: args.path
|
|
||||||
}
|
}
|
||||||
set(obj)!
|
set(obj)!
|
||||||
return get(name: args.name)!
|
return get(name: args.name)!
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ const default = true
|
|||||||
@[heap]
|
@[heap]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub mut:
|
pub mut:
|
||||||
name string = 'default' // Workspace name
|
name string = 'default' // Workspace name
|
||||||
base_path string // Base path of the workspace
|
children []HeropromptChild // List of directories and files in this workspace
|
||||||
children []HeropromptChild // List of directories and files in this workspace
|
created time.Time // Time of creation
|
||||||
created time.Time // Time of creation
|
updated time.Time // Time of last update
|
||||||
updated time.Time // Time of last update
|
is_saved bool
|
||||||
is_saved bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// your checking & initialization code if needed
|
// your checking & initialization code if needed
|
||||||
@@ -41,10 +40,9 @@ pub fn heroscript_loads(heroscript string) !Workspace {
|
|||||||
mut p := action.params
|
mut p := action.params
|
||||||
|
|
||||||
return Workspace{
|
return Workspace{
|
||||||
name: p.get_default('name', 'default')!
|
name: p.get_default('name', 'default')!
|
||||||
base_path: p.get_default('base_path', '')!
|
created: time.now()
|
||||||
created: time.now()
|
updated: time.now()
|
||||||
updated: time.now()
|
children: []HeropromptChild{}
|
||||||
children: []HeropromptChild{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
|
|||||||
name := os.base(abs_path)
|
name := os.base(abs_path)
|
||||||
|
|
||||||
for child in wsp.children {
|
for child in wsp.children {
|
||||||
if child.name == name {
|
if child.path.cat == .dir && child.path.path == abs_path {
|
||||||
return error('another directory with the same name already exists: ${name}')
|
return error('the directory is already added to the workspace')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +171,11 @@ pub mut:
|
|||||||
|
|
||||||
pub fn (wsp &Workspace) update_workspace(args UpdateParams) !&Workspace {
|
pub fn (wsp &Workspace) update_workspace(args UpdateParams) !&Workspace {
|
||||||
mut updated := Workspace{
|
mut updated := Workspace{
|
||||||
name: if args.name.len > 0 { args.name } else { wsp.name }
|
name: if args.name.len > 0 { args.name } else { wsp.name }
|
||||||
base_path: if args.base_path.len > 0 { args.base_path } else { wsp.base_path }
|
children: wsp.children
|
||||||
children: wsp.children
|
created: wsp.created
|
||||||
created: wsp.created
|
updated: time.now()
|
||||||
updated: time.now()
|
is_saved: true
|
||||||
is_saved: true
|
|
||||||
}
|
}
|
||||||
// if name changed, delete old key first
|
// if name changed, delete old key first
|
||||||
if updated.name != wsp.name {
|
if updated.name != wsp.name {
|
||||||
@@ -221,10 +220,10 @@ pub:
|
|||||||
typ string @[json: 'type']
|
typ string @[json: 'type']
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem {
|
pub fn (wsp &Workspace) list_dir(base_path string, rel_path string) ![]ListItem {
|
||||||
// Create an ignore matcher with default patterns
|
// Create an ignore matcher with default patterns
|
||||||
ignore_matcher := codewalker.gitignore_matcher_new()
|
ignore_matcher := codewalker.gitignore_matcher_new()
|
||||||
items := codewalker.list_directory_filtered(wsp.base_path, rel_path, &ignore_matcher)!
|
items := codewalker.list_directory_filtered(base_path, rel_path, &ignore_matcher)!
|
||||||
mut out := []ListItem{}
|
mut out := []ListItem{}
|
||||||
for item in items {
|
for item in items {
|
||||||
out << ListItem{
|
out << ListItem{
|
||||||
@@ -235,10 +234,6 @@ pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (wsp &Workspace) list() ![]ListItem {
|
|
||||||
return wsp.list_dir('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the currently selected children (copy)
|
// Get the currently selected children (copy)
|
||||||
pub fn (wsp Workspace) selected_children() []HeropromptChild {
|
pub fn (wsp Workspace) selected_children() []HeropromptChild {
|
||||||
return wsp.children.clone()
|
return wsp.children.clone()
|
||||||
@@ -319,17 +314,29 @@ fn (wsp Workspace) build_file_map() string {
|
|||||||
// derive a parent path for display
|
// derive a parent path for display
|
||||||
mut parent_path := ''
|
mut parent_path := ''
|
||||||
if roots.len > 0 {
|
if roots.len > 0 {
|
||||||
base_path := roots[0].path.path
|
if roots.len == 1 {
|
||||||
parent_path = if base_path.contains('/') {
|
// Single root - show parent directory
|
||||||
base_path.split('/')[..base_path.split('/').len - 1].join('/')
|
base_path := roots[0].path.path
|
||||||
|
parent_path = if base_path.contains('/') {
|
||||||
|
base_path.split('/')[..base_path.split('/').len - 1].join('/')
|
||||||
|
} else {
|
||||||
|
base_path
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
base_path
|
// Multiple roots - show all root paths, comma-separated
|
||||||
|
mut root_paths := []string{}
|
||||||
|
for r in roots {
|
||||||
|
root_paths << r.path.path
|
||||||
|
}
|
||||||
|
parent_path = root_paths.join(', ')
|
||||||
|
// Truncate if too long for UI display
|
||||||
|
if parent_path.len > 100 {
|
||||||
|
parent_path = parent_path[..97] + '...'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no roots; show workspace base if set, else the parent of first file
|
// no roots; show the parent of first file if available
|
||||||
parent_path = if wsp.base_path.len > 0 {
|
parent_path = if files_only.len > 0 {
|
||||||
wsp.base_path
|
|
||||||
} else if files_only.len > 0 {
|
|
||||||
os.dir(files_only[0].path.path)
|
os.dir(files_only[0].path.path)
|
||||||
} else {
|
} else {
|
||||||
''
|
''
|
||||||
@@ -380,27 +387,27 @@ fn (wsp Workspace) build_file_map() string {
|
|||||||
file_map += ' | Extensions: ${extensions_summary}'
|
file_map += ' | Extensions: ${extensions_summary}'
|
||||||
}
|
}
|
||||||
file_map += '\n\n'
|
file_map += '\n\n'
|
||||||
// Render selected structure
|
// Build a comprehensive tree that includes all files from selected directories
|
||||||
if roots.len > 0 {
|
mut all_file_paths := []string{}
|
||||||
mut root_paths := []string{}
|
|
||||||
for r in roots {
|
// For each selected directory, get all files within it
|
||||||
root_paths << r.path.path
|
for r in roots {
|
||||||
|
mut cw := codewalker.new(codewalker.CodeWalkerArgs{}) or { continue }
|
||||||
|
fm := cw.filemap_get(path: r.path.path) or { continue }
|
||||||
|
for rel_path, _ in fm.content {
|
||||||
|
abs_file_path := os.join_path(r.path.path, rel_path)
|
||||||
|
all_file_paths << abs_file_path
|
||||||
}
|
}
|
||||||
file_map += codewalker.build_file_tree_fs(root_paths, '')
|
|
||||||
}
|
}
|
||||||
// If there are only standalone selected files (no selected dirs),
|
|
||||||
// build a minimal tree via codewalker relative to the workspace base.
|
// Add all standalone file paths
|
||||||
if files_only.len > 0 && roots.len == 0 {
|
for fo in files_only {
|
||||||
mut paths := []string{}
|
all_file_paths << fo.path.path
|
||||||
for fo in files_only {
|
}
|
||||||
paths << fo.path.path
|
|
||||||
}
|
if all_file_paths.len > 0 {
|
||||||
file_map += codewalker.build_selected_tree(paths, wsp.base_path)
|
// Build a tree that shows all files in their proper directory structure
|
||||||
} else if files_only.len > 0 && roots.len > 0 {
|
file_map += codewalker.build_file_tree_fs(all_file_paths, '')
|
||||||
// Keep listing absolute paths for standalone files when directories are also selected.
|
|
||||||
for fo in files_only {
|
|
||||||
file_map += fo.path.path + ' *\n'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file_map
|
return file_map
|
||||||
|
|||||||
121
lib/develop/heroprompt/heroprompt_workspace_test.v
Normal file
121
lib/develop/heroprompt/heroprompt_workspace_test.v
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
module heroprompt
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
fn test_multiple_dirs_same_name() ! {
|
||||||
|
// Create two temporary folders with the same basename "proj"
|
||||||
|
temp_base := os.temp_dir()
|
||||||
|
dir1 := os.join_path(temp_base, 'test_heroprompt_1', 'proj')
|
||||||
|
dir2 := os.join_path(temp_base, 'test_heroprompt_2', 'proj')
|
||||||
|
|
||||||
|
// Ensure directories exist
|
||||||
|
os.mkdir_all(dir1)!
|
||||||
|
os.mkdir_all(dir2)!
|
||||||
|
|
||||||
|
// Create test files in each directory
|
||||||
|
os.write_file(os.join_path(dir1, 'file1.txt'), 'content1')!
|
||||||
|
os.write_file(os.join_path(dir2, 'file2.txt'), 'content2')!
|
||||||
|
|
||||||
|
defer {
|
||||||
|
// Cleanup
|
||||||
|
os.rmdir_all(os.join_path(temp_base, 'test_heroprompt_1')) or {}
|
||||||
|
os.rmdir_all(os.join_path(temp_base, 'test_heroprompt_2')) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mut ws := Workspace{
|
||||||
|
name: 'testws'
|
||||||
|
children: []HeropromptChild{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First dir – should succeed
|
||||||
|
child1 := ws.add_dir(path: dir1)!
|
||||||
|
assert ws.children.len == 1
|
||||||
|
assert child1.name == 'proj'
|
||||||
|
assert child1.path.path == os.real_path(dir1)
|
||||||
|
|
||||||
|
// Second dir – same basename, different absolute path – should also succeed
|
||||||
|
child2 := ws.add_dir(path: dir2)!
|
||||||
|
assert ws.children.len == 2
|
||||||
|
assert child2.name == 'proj'
|
||||||
|
assert child2.path.path == os.real_path(dir2)
|
||||||
|
|
||||||
|
// Verify both children have different absolute paths
|
||||||
|
assert child1.path.path != child2.path.path
|
||||||
|
|
||||||
|
// Try to add the same directory again – should fail
|
||||||
|
ws.add_dir(path: dir1) or {
|
||||||
|
assert err.msg().contains('already added to the workspace')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false, 'Expected error when adding same directory twice'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_build_file_map_multiple_roots() ! {
|
||||||
|
// Create temporary directories
|
||||||
|
temp_base := os.temp_dir()
|
||||||
|
dir1 := os.join_path(temp_base, 'test_map_1', 'src')
|
||||||
|
dir2 := os.join_path(temp_base, 'test_map_2', 'src')
|
||||||
|
|
||||||
|
os.mkdir_all(dir1)!
|
||||||
|
os.mkdir_all(dir2)!
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
os.write_file(os.join_path(dir1, 'main.v'), 'fn main() { println("hello from dir1") }')!
|
||||||
|
os.write_file(os.join_path(dir2, 'app.v'), 'fn app() { println("hello from dir2") }')!
|
||||||
|
|
||||||
|
defer {
|
||||||
|
os.rmdir_all(os.join_path(temp_base, 'test_map_1')) or {}
|
||||||
|
os.rmdir_all(os.join_path(temp_base, 'test_map_2')) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mut ws := Workspace{
|
||||||
|
name: 'testws_map'
|
||||||
|
children: []HeropromptChild{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add both directories
|
||||||
|
ws.add_dir(path: dir1)!
|
||||||
|
ws.add_dir(path: dir2)!
|
||||||
|
|
||||||
|
// Build file map
|
||||||
|
file_map := ws.build_file_map()
|
||||||
|
|
||||||
|
// Should contain both directory paths in the parent_path
|
||||||
|
assert file_map.contains(os.real_path(dir1))
|
||||||
|
assert file_map.contains(os.real_path(dir2))
|
||||||
|
|
||||||
|
// Should show correct file count (2 files total)
|
||||||
|
assert file_map.contains('Selected Files: 2')
|
||||||
|
|
||||||
|
// Should contain both file extensions
|
||||||
|
assert file_map.contains('v(2)')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_single_dir_backward_compatibility() ! {
|
||||||
|
// Test that single directory workspaces still work as before
|
||||||
|
temp_base := os.temp_dir()
|
||||||
|
test_dir := os.join_path(temp_base, 'test_single', 'myproject')
|
||||||
|
|
||||||
|
os.mkdir_all(test_dir)!
|
||||||
|
os.write_file(os.join_path(test_dir, 'main.v'), 'fn main() { println("single dir test") }')!
|
||||||
|
|
||||||
|
defer {
|
||||||
|
os.rmdir_all(os.join_path(temp_base, 'test_single')) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mut ws := Workspace{
|
||||||
|
name: 'testws_single'
|
||||||
|
children: []HeropromptChild{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add single directory
|
||||||
|
child := ws.add_dir(path: test_dir)!
|
||||||
|
assert ws.children.len == 1
|
||||||
|
assert child.name == 'myproject'
|
||||||
|
|
||||||
|
// Build file map - should work as before for single directory
|
||||||
|
file_map := ws.build_file_map()
|
||||||
|
assert file_map.contains('Selected Files: 1')
|
||||||
|
// Just check that the file map is not empty and contains some content
|
||||||
|
assert file_map.len > 0
|
||||||
|
}
|
||||||
@@ -82,26 +82,17 @@ pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
|
|||||||
@['/api/heroprompt/workspaces'; post]
|
@['/api/heroprompt/workspaces'; post]
|
||||||
pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
|
||||||
name_input := ctx.form['name'] or { '' }
|
name_input := ctx.form['name'] or { '' }
|
||||||
base_path_in := ctx.form['base_path'] or { '' }
|
|
||||||
if base_path_in.len == 0 {
|
|
||||||
return ctx.text(json_error('base_path required'))
|
|
||||||
}
|
|
||||||
|
|
||||||
base_path := expand_home_path(base_path_in)
|
// Name is now required
|
||||||
|
|
||||||
// If no name provided, generate a random name
|
|
||||||
mut name := name_input.trim(' \t\n\r')
|
mut name := name_input.trim(' \t\n\r')
|
||||||
if name.len == 0 {
|
if name.len == 0 {
|
||||||
name = hp.generate_random_workspace_name()
|
return ctx.text(json_error('workspace name is required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
wsp := hp.get(name: name, create: true, path: base_path) or {
|
wsp := hp.get(name: name, create: true) or { return ctx.text(json_error('create failed')) }
|
||||||
return ctx.text(json_error('create failed'))
|
|
||||||
}
|
|
||||||
ctx.set_content_type('application/json')
|
ctx.set_content_type('application/json')
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': wsp.name
|
'name': wsp.name
|
||||||
'base_path': wsp.base_path
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +104,6 @@ pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
|
|||||||
ctx.set_content_type('application/json')
|
ctx.set_content_type('application/json')
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': wsp.name
|
'name': wsp.name
|
||||||
'base_path': wsp.base_path
|
|
||||||
'selected_files': wsp.selected_children().len.str()
|
'selected_files': wsp.selected_children().len.str()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -125,19 +115,15 @@ pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
new_name := ctx.form['name'] or { name }
|
new_name := ctx.form['name'] or { name }
|
||||||
new_base_path_in := ctx.form['base_path'] or { wsp.base_path }
|
|
||||||
new_base_path := expand_home_path(new_base_path_in)
|
|
||||||
|
|
||||||
// Update the workspace using the update_workspace method
|
// Update the workspace using the update_workspace method
|
||||||
updated_wsp := wsp.update_workspace(
|
updated_wsp := wsp.update_workspace(
|
||||||
name: new_name
|
name: new_name
|
||||||
base_path: new_base_path
|
|
||||||
) or { return ctx.text(json_error('failed to update workspace')) }
|
) or { return ctx.text(json_error('failed to update workspace')) }
|
||||||
|
|
||||||
ctx.set_content_type('application/json')
|
ctx.set_content_type('application/json')
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': updated_wsp.name
|
'name': updated_wsp.name
|
||||||
'base_path': updated_wsp.base_path
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,13 +145,21 @@ pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result
|
|||||||
pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
wsname := ctx.query['name'] or { 'default' }
|
||||||
path_q := ctx.query['path'] or { '' }
|
path_q := ctx.query['path'] or { '' }
|
||||||
|
base_path := ctx.query['base'] or { '' }
|
||||||
|
|
||||||
|
if base_path.len == 0 {
|
||||||
|
return ctx.text(json_error('base path is required'))
|
||||||
|
}
|
||||||
|
|
||||||
mut wsp := hp.get(name: wsname, create: false) or {
|
mut wsp := hp.get(name: wsname, create: false) or {
|
||||||
return ctx.text(json_error('workspace not found'))
|
return ctx.text(json_error('workspace not found'))
|
||||||
}
|
}
|
||||||
items := wsp.list_dir(path_q) or { return ctx.text(json_error('cannot list directory')) }
|
items := wsp.list_dir(base_path, path_q) or {
|
||||||
|
return ctx.text(json_error('cannot list directory'))
|
||||||
|
}
|
||||||
ctx.set_content_type('application/json')
|
ctx.set_content_type('application/json')
|
||||||
return ctx.text(json.encode(DirResp{
|
return ctx.text(json.encode(DirResp{
|
||||||
path: if path_q.len > 0 { path_q } else { wsp.base_path }
|
path: if path_q.len > 0 { path_q } else { base_path }
|
||||||
items: items
|
items: items
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -177,15 +171,9 @@ pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result {
|
|||||||
if path_q.len == 0 {
|
if path_q.len == 0 {
|
||||||
return ctx.text(json_error('path required'))
|
return ctx.text(json_error('path required'))
|
||||||
}
|
}
|
||||||
mut base := ''
|
|
||||||
if wsp := hp.get(name: wsname, create: false) {
|
// Use the path directly (should be absolute)
|
||||||
base = wsp.base_path
|
file_path := path_q
|
||||||
}
|
|
||||||
mut file_path := if !os.is_abs_path(path_q) && base.len > 0 {
|
|
||||||
os.join_path(base, path_q)
|
|
||||||
} else {
|
|
||||||
path_q
|
|
||||||
}
|
|
||||||
if !os.is_file(file_path) {
|
if !os.is_file(file_path) {
|
||||||
return ctx.text(json_error('not a file'))
|
return ctx.text(json_error('not a file'))
|
||||||
}
|
}
|
||||||
@@ -268,10 +256,16 @@ pub fn (app &App) api_heroprompt_sync_selection(mut ctx Context, name string) ve
|
|||||||
@['/api/heroprompt/workspaces/:name/search'; get]
|
@['/api/heroprompt/workspaces/:name/search'; get]
|
||||||
pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result {
|
||||||
query := ctx.query['q'] or { '' }
|
query := ctx.query['q'] or { '' }
|
||||||
|
base_path := ctx.query['base'] or { '' }
|
||||||
|
|
||||||
if query.len == 0 {
|
if query.len == 0 {
|
||||||
return ctx.text(json_error('search query required'))
|
return ctx.text(json_error('search query required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if base_path.len == 0 {
|
||||||
|
return ctx.text(json_error('base path required for search'))
|
||||||
|
}
|
||||||
|
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := hp.get(name: name, create: false) or {
|
||||||
return ctx.text(json_error('workspace not found'))
|
return ctx.text(json_error('workspace not found'))
|
||||||
}
|
}
|
||||||
@@ -281,7 +275,7 @@ pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result
|
|||||||
query_lower := query.to_lower()
|
query_lower := query.to_lower()
|
||||||
|
|
||||||
// Recursive function to search files
|
// Recursive function to search files
|
||||||
search_directory(wsp.base_path, wsp.base_path, query_lower, mut results)
|
search_directory(base_path, base_path, query_lower, mut results)
|
||||||
|
|
||||||
ctx.set_content_type('application/json')
|
ctx.set_content_type('application/json')
|
||||||
|
|
||||||
@@ -303,3 +297,14 @@ pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result
|
|||||||
response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}'
|
response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}'
|
||||||
return ctx.text(response)
|
return ctx.text(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@['/api/heroprompt/workspaces/:name/children'; get]
|
||||||
|
pub fn (app &App) api_heroprompt_get_children(mut ctx Context, name string) veb.Result {
|
||||||
|
wsp := hp.get(name: name, create: false) or {
|
||||||
|
return ctx.text(json_error('workspace not found'))
|
||||||
|
}
|
||||||
|
|
||||||
|
children := wsp.selected_children()
|
||||||
|
ctx.set_content_type('application/json')
|
||||||
|
return ctx.text(json.encode(children))
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class SimpleFileTree {
|
|||||||
async loadChildren(parentPath) {
|
async loadChildren(parentPath) {
|
||||||
// Always reload children to ensure fresh data
|
// Always reload children to ensure fresh data
|
||||||
console.log('Loading children for:', parentPath);
|
console.log('Loading children for:', parentPath);
|
||||||
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
|
const r = await api(`/api/heroprompt/directory?name=${currentWs}&base=${encodeURIComponent(parentPath)}&path=`);
|
||||||
|
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
console.warn('Failed to load directory:', parentPath, r.error);
|
console.warn('Failed to load directory:', parentPath, r.error);
|
||||||
@@ -395,6 +395,9 @@ class SimpleFileTree {
|
|||||||
// Get file stats (mock data for now - could be enhanced with real file stats)
|
// Get file stats (mock data for now - could be enhanced with real file stats)
|
||||||
const stats = this.getFileStats(path);
|
const stats = this.getFileStats(path);
|
||||||
|
|
||||||
|
// Show full path for directories to help differentiate between same-named directories
|
||||||
|
const displayPath = isDirectory ? path : path;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="file-card-header">
|
<div class="file-card-header">
|
||||||
<div class="file-card-icon">
|
<div class="file-card-icon">
|
||||||
@@ -402,7 +405,7 @@ class SimpleFileTree {
|
|||||||
</div>
|
</div>
|
||||||
<div class="file-card-info">
|
<div class="file-card-info">
|
||||||
<h4 class="file-card-name">${fileName}</h4>
|
<h4 class="file-card-name">${fileName}</h4>
|
||||||
<p class="file-card-path">${path}</p>
|
<p class="file-card-path">${displayPath}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-card-metadata">
|
<div class="file-card-metadata">
|
||||||
@@ -825,6 +828,66 @@ class SimpleFileTree {
|
|||||||
|
|
||||||
this.updateSelectionUI();
|
this.updateSelectionUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renderWorkspaceDirectories(directories) {
|
||||||
|
this.container.innerHTML = '<div class="loading">Loading workspace directories...</div>';
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
this.loadedPaths.clear();
|
||||||
|
this.expandedDirs.clear();
|
||||||
|
expandedDirs.clear();
|
||||||
|
|
||||||
|
if (!directories || directories.length === 0) {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>No directories added yet</p>
|
||||||
|
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create document fragment for efficient DOM manipulation
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
// Create elements for each workspace directory
|
||||||
|
for (const dir of directories) {
|
||||||
|
if (!dir.path || dir.path.cat !== 'dir') continue;
|
||||||
|
|
||||||
|
const dirPath = dir.path.path;
|
||||||
|
const dirName = dir.name || dirPath.split('/').pop();
|
||||||
|
|
||||||
|
// Create a directory item that can be expanded
|
||||||
|
const item = {
|
||||||
|
name: dirName,
|
||||||
|
type: 'directory'
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = this.createFileItem(item, dirPath, 0);
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.transform = 'translateY(-10px)';
|
||||||
|
element.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
|
|
||||||
|
fragment.appendChild(element);
|
||||||
|
elements.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear container and add all elements at once
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.container.appendChild(fragment);
|
||||||
|
|
||||||
|
// Trigger staggered animations
|
||||||
|
elements.forEach((element, i) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.opacity = '1';
|
||||||
|
element.style.transform = 'translateY(0)';
|
||||||
|
}, i * 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateSelectionUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global tree instance
|
// Global tree instance
|
||||||
@@ -886,10 +949,56 @@ async function initWorkspace() {
|
|||||||
const sel = el('workspaceSelect');
|
const sel = el('workspaceSelect');
|
||||||
if (sel) sel.value = currentWs;
|
if (sel) sel.value = currentWs;
|
||||||
|
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
// Load and display workspace directories
|
||||||
const base = info?.base_path || '';
|
await loadWorkspaceDirectories();
|
||||||
if (base && fileTree) {
|
}
|
||||||
await fileTree.render(base);
|
|
||||||
|
async function loadWorkspaceDirectories() {
|
||||||
|
const treeEl = el('tree');
|
||||||
|
if (!treeEl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const children = await api(`/api/heroprompt/workspaces/${currentWs}/children`);
|
||||||
|
|
||||||
|
if (children.error) {
|
||||||
|
console.warn('Failed to load workspace children:', children.error);
|
||||||
|
treeEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>No directories added yet</p>
|
||||||
|
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter only directories
|
||||||
|
const directories = children.filter(child => child.path && child.path.cat === 'dir');
|
||||||
|
|
||||||
|
if (directories.length === 0) {
|
||||||
|
treeEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>No directories added yet</p>
|
||||||
|
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file tree with workspace directories as roots
|
||||||
|
if (fileTree) {
|
||||||
|
await fileTree.renderWorkspaceDirectories(directories);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading workspace directories:', error);
|
||||||
|
treeEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>Error loading directories</p>
|
||||||
|
<small>Please try refreshing the page</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,41 +1061,64 @@ async function copyPrompt() {
|
|||||||
const outputEl = el('promptOutput');
|
const outputEl = el('promptOutput');
|
||||||
if (!outputEl) {
|
if (!outputEl) {
|
||||||
console.warn('Prompt output element not found');
|
console.warn('Prompt output element not found');
|
||||||
|
showStatus('Copy failed - element not found', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = outputEl.textContent;
|
// Grab the visible prompt text, stripping HTML and empty-state placeholders
|
||||||
console.log('text', text);
|
const text = outputEl.innerText.trim();
|
||||||
if (!text || text.trim().length === 0 || text.includes('No files selected') || text.includes('Generated prompt will appear here')) {
|
if (!text || text.includes('Generated prompt will appear here') || text.includes('No files selected')) {
|
||||||
console.warn('No valid content to copy');
|
showStatus('Nothing to copy', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!navigator.clipboard) {
|
// Try the modern Clipboard API first
|
||||||
// Fallback for older browsers
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
fallbackCopyToClipboard(text);
|
try {
|
||||||
return;
|
await navigator.clipboard.writeText(text);
|
||||||
|
showStatus('Prompt copied to clipboard!', 'success');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Clipboard API failed, falling back', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to hidden textarea method
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed'; // avoid scrolling to bottom
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
const successful = document.execCommand('copy');
|
||||||
|
showStatus(successful ? 'Prompt copied!' : 'Copy failed', successful ? 'success' : 'error');
|
||||||
// Show success feedback
|
|
||||||
const originalContent = outputEl.innerHTML;
|
|
||||||
outputEl.innerHTML = '<div class="success-message">Prompt copied to clipboard!</div>';
|
|
||||||
setTimeout(() => {
|
|
||||||
outputEl.innerHTML = originalContent;
|
|
||||||
}, 2000);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Copy failed', e);
|
console.error('Fallback copy failed', e);
|
||||||
const originalContent = outputEl.innerHTML;
|
showStatus('Copy failed', 'error');
|
||||||
outputEl.innerHTML = '<div class="error-message">Failed to copy prompt</div>';
|
} finally {
|
||||||
setTimeout(() => {
|
document.body.removeChild(textarea);
|
||||||
outputEl.innerHTML = originalContent;
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper – show a transient message inside the output pane */
|
||||||
|
function showStatus(msg, type = 'info') {
|
||||||
|
const out = el('promptOutput');
|
||||||
|
if (!out) return;
|
||||||
|
|
||||||
|
const original = out.innerHTML;
|
||||||
|
const statusClass = type === 'success' ? 'success-message' :
|
||||||
|
type === 'error' ? 'error-message' :
|
||||||
|
type === 'warning' ? 'warning-message' : 'info-message';
|
||||||
|
|
||||||
|
out.innerHTML = `<div class="${statusClass}">${msg}</div>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
out.innerHTML = original;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
// Global fallback function for clipboard operations
|
// Global fallback function for clipboard operations
|
||||||
function fallbackCopyToClipboard(text) {
|
function fallbackCopyToClipboard(text) {
|
||||||
const textArea = document.createElement('textarea');
|
const textArea = document.createElement('textarea');
|
||||||
@@ -1049,11 +1181,9 @@ async function deleteWorkspace(workspaceName) {
|
|||||||
currentWs = names[0];
|
currentWs = names[0];
|
||||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||||
await reloadWorkspaces();
|
await reloadWorkspaces();
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
|
||||||
const base = info?.base_path || '';
|
// Load directories for new current workspace
|
||||||
if (base && fileTree) {
|
await loadWorkspaceDirectories();
|
||||||
await fileTree.render(base);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,15 +1194,12 @@ async function deleteWorkspace(workspaceName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWorkspace(workspaceName, newName, newPath) {
|
async function updateWorkspace(workspaceName, newName) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (newName && newName !== workspaceName) {
|
if (newName && newName !== workspaceName) {
|
||||||
formData.append('name', newName);
|
formData.append('name', newName);
|
||||||
}
|
}
|
||||||
if (newPath) {
|
|
||||||
formData.append('base_path', newPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedName = encodeURIComponent(workspaceName);
|
const encodedName = encodeURIComponent(workspaceName);
|
||||||
const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, {
|
const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, {
|
||||||
@@ -1095,12 +1222,6 @@ async function updateWorkspace(workspaceName, newName, newPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await reloadWorkspaces();
|
await reloadWorkspaces();
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
|
||||||
const base = info?.base_path || '';
|
|
||||||
if (base && fileTree) {
|
|
||||||
await fileTree.render(base);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Update workspace failed', e);
|
console.warn('Update workspace failed', e);
|
||||||
@@ -1135,11 +1256,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
workspaceSelect.addEventListener('change', async (e) => {
|
workspaceSelect.addEventListener('change', async (e) => {
|
||||||
currentWs = e.target.value;
|
currentWs = e.target.value;
|
||||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
|
||||||
const base = info?.base_path || '';
|
// Load directories for the new workspace
|
||||||
if (base && fileTree) {
|
await loadWorkspaceDirectories();
|
||||||
await fileTree.render(base);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,11 +1273,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const refreshExplorerBtn = el('refreshExplorer');
|
const refreshExplorerBtn = el('refreshExplorer');
|
||||||
if (refreshExplorerBtn) {
|
if (refreshExplorerBtn) {
|
||||||
refreshExplorerBtn.addEventListener('click', async () => {
|
refreshExplorerBtn.addEventListener('click', async () => {
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
// Reload workspace directories
|
||||||
const base = info?.base_path || '';
|
await loadWorkspaceDirectories();
|
||||||
if (base && fileTree) {
|
|
||||||
await fileTree.render(base);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,11 +1338,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (wsCreateBtn) {
|
if (wsCreateBtn) {
|
||||||
wsCreateBtn.addEventListener('click', () => {
|
wsCreateBtn.addEventListener('click', () => {
|
||||||
const nameEl = el('wcName');
|
const nameEl = el('wcName');
|
||||||
const pathEl = el('wcPath');
|
|
||||||
const errorEl = el('wcError');
|
const errorEl = el('wcError');
|
||||||
|
|
||||||
if (nameEl) nameEl.value = '';
|
if (nameEl) nameEl.value = '';
|
||||||
if (pathEl) pathEl.value = '';
|
|
||||||
if (errorEl) errorEl.textContent = '';
|
if (errorEl) errorEl.textContent = '';
|
||||||
|
|
||||||
showModal('wsCreate');
|
showModal('wsCreate');
|
||||||
@@ -1237,16 +1351,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (wcCreateBtn) {
|
if (wcCreateBtn) {
|
||||||
wcCreateBtn.addEventListener('click', async () => {
|
wcCreateBtn.addEventListener('click', async () => {
|
||||||
const name = el('wcName')?.value?.trim() || '';
|
const name = el('wcName')?.value?.trim() || '';
|
||||||
const path = el('wcPath')?.value?.trim() || '';
|
|
||||||
const errorEl = el('wcError');
|
const errorEl = el('wcError');
|
||||||
|
|
||||||
if (!path) {
|
if (!name) {
|
||||||
if (errorEl) errorEl.textContent = 'Path is required.';
|
if (errorEl) errorEl.textContent = 'Workspace name is required.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = { base_path: path };
|
const formData = { name: name };
|
||||||
if (name) formData.name = name;
|
|
||||||
|
|
||||||
const resp = await post('/api/heroprompt/workspaces', formData);
|
const resp = await post('/api/heroprompt/workspaces', formData);
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
@@ -1258,10 +1370,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||||
await reloadWorkspaces();
|
await reloadWorkspaces();
|
||||||
|
|
||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
// Clear the file tree since new workspace has no directories yet
|
||||||
const base = info?.base_path || '';
|
if (fileTree) {
|
||||||
if (base && fileTree) {
|
const treeEl = el('tree');
|
||||||
await fileTree.render(base);
|
if (treeEl) {
|
||||||
|
treeEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>No directories added yet</p>
|
||||||
|
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideModal('wsCreate');
|
hideModal('wsCreate');
|
||||||
@@ -1275,11 +1395,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||||
if (info && !info.error) {
|
if (info && !info.error) {
|
||||||
const nameEl = el('wdName');
|
const nameEl = el('wdName');
|
||||||
const pathEl = el('wdPath');
|
|
||||||
const errorEl = el('wdError');
|
const errorEl = el('wdError');
|
||||||
|
|
||||||
if (nameEl) nameEl.value = info.name || currentWs;
|
if (nameEl) nameEl.value = info.name || currentWs;
|
||||||
if (pathEl) pathEl.value = info.base_path || '';
|
|
||||||
if (errorEl) errorEl.textContent = '';
|
if (errorEl) errorEl.textContent = '';
|
||||||
|
|
||||||
showModal('wsDetails');
|
showModal('wsDetails');
|
||||||
@@ -1292,15 +1410,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (wdUpdateBtn) {
|
if (wdUpdateBtn) {
|
||||||
wdUpdateBtn.addEventListener('click', async () => {
|
wdUpdateBtn.addEventListener('click', async () => {
|
||||||
const name = el('wdName')?.value?.trim() || '';
|
const name = el('wdName')?.value?.trim() || '';
|
||||||
const path = el('wdPath')?.value?.trim() || '';
|
|
||||||
const errorEl = el('wdError');
|
const errorEl = el('wdError');
|
||||||
|
|
||||||
if (!path) {
|
if (!name) {
|
||||||
if (errorEl) errorEl.textContent = 'Path is required.';
|
if (errorEl) errorEl.textContent = 'Workspace name is required.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await updateWorkspace(currentWs, name, path);
|
const result = await updateWorkspace(currentWs, name);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (errorEl) errorEl.textContent = result.error;
|
if (errorEl) errorEl.textContent = result.error;
|
||||||
return;
|
return;
|
||||||
@@ -1326,6 +1443,52 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Directory functionality
|
||||||
|
const addDirBtn = el('addDirBtn');
|
||||||
|
if (addDirBtn) {
|
||||||
|
addDirBtn.addEventListener('click', () => {
|
||||||
|
const pathEl = el('addDirPath');
|
||||||
|
const errorEl = el('addDirError');
|
||||||
|
|
||||||
|
if (pathEl) pathEl.value = '';
|
||||||
|
if (errorEl) errorEl.textContent = '';
|
||||||
|
|
||||||
|
showModal('addDirModal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addDirConfirm = el('addDirConfirm');
|
||||||
|
if (addDirConfirm) {
|
||||||
|
addDirConfirm.addEventListener('click', async () => {
|
||||||
|
const path = el('addDirPath')?.value?.trim() || '';
|
||||||
|
const errorEl = el('addDirError');
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
if (errorEl) errorEl.textContent = 'Directory path is required.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add directory via API
|
||||||
|
const result = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/dirs`, {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
if (errorEl) errorEl.textContent = result.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - close modal and refresh the file tree
|
||||||
|
hideModal('addDirModal');
|
||||||
|
|
||||||
|
// Reload workspace directories to show the newly added directory
|
||||||
|
await loadWorkspaceDirectories();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showStatus('Directory added successfully!', 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Chat functionality
|
// Chat functionality
|
||||||
initChatInterface();
|
initChatInterface();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-actions">
|
<div class="selection-actions">
|
||||||
|
<button id="addDirBtn" class="btn btn-sm btn-primary" title="Add Directory">
|
||||||
|
<i class="icon-plus"></i>
|
||||||
|
Add Dir
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,12 +314,10 @@ Example:
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="wcName" class="form-label">Workspace Name (optional)</label>
|
<label for="wcName" class="form-label">Workspace Name (required)</label>
|
||||||
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name">
|
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name" required>
|
||||||
</div>
|
<div class="form-text">Choose a unique name for your workspace. You can add directories to it
|
||||||
<div class="mb-3">
|
after creation.</div>
|
||||||
<label for="wcPath" class="form-label">Base Path (required)</label>
|
|
||||||
<input type="text" class="form-control" id="wcPath" placeholder="Enter base directory path">
|
|
||||||
</div>
|
</div>
|
||||||
<div id="wcError" class="text-danger small"></div>
|
<div id="wcError" class="text-danger small"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,10 +342,6 @@ Example:
|
|||||||
<label for="wdName" class="form-label">Workspace Name</label>
|
<label for="wdName" class="form-label">Workspace Name</label>
|
||||||
<input type="text" class="form-control" id="wdName">
|
<input type="text" class="form-control" id="wdName">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="wdPath" class="form-label">Base Path</label>
|
|
||||||
<input type="text" class="form-control" id="wdPath">
|
|
||||||
</div>
|
|
||||||
<div id="wdError" class="text-danger small"></div>
|
<div id="wdError" class="text-danger small"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -392,6 +390,32 @@ Example:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Directory Modal -->
|
||||||
|
<div class="modal fade" id="addDirModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Directory to Workspace</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="addDirPath" class="form-label">Directory Path</label>
|
||||||
|
<input type="text" class="form-control" id="addDirPath"
|
||||||
|
placeholder="Enter directory path (e.g., /path/to/directory)">
|
||||||
|
<div class="form-text">Enter the full path to the directory you want to add to the workspace.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="addDirError" class="text-danger small"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="addDirConfirm">Add Directory</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user