feat: implement full heroprompt workspace management
- Add create, save, get, list, and delete for workspaces - Enable adding and removing files/dirs by path or name - Integrate codewalker for recursive file discovery - Make workspaces stateful with created/updated timestamps - Update example to demonstrate new lifecycle methods
This commit is contained in:
1522
debug.logs
Normal file
1522
debug.logs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,46 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.develop.heroprompt
|
||||
import freeflowuniverse.herolib.core.playbook
|
||||
import os
|
||||
|
||||
// heroscript_config := '
|
||||
// !!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib"
|
||||
// '
|
||||
// mut plbook := playbook.new(
|
||||
// text: heroscript_config
|
||||
// )!
|
||||
|
||||
// heroprompt.play(mut plbook)!
|
||||
|
||||
// mut workspace1 := heroprompt.new_workspace(
|
||||
// mut workspace := heroprompt.new(
|
||||
// path: '${os.home_dir()}/code/github/freeflowuniverse/herolib'
|
||||
// name: 'workspace'
|
||||
// )!
|
||||
|
||||
mut workspace2 := heroprompt.get(
|
||||
name: 'test workspace'
|
||||
mut workspace := heroprompt.get(
|
||||
name: 'example_ws'
|
||||
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib'
|
||||
create: true
|
||||
)!
|
||||
|
||||
// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker')!
|
||||
// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/docker_ubuntu_install.sh')!
|
||||
println('workspace (initial): ${workspace}')
|
||||
println('selected (initial): ${workspace.selected_children()}')
|
||||
|
||||
// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib')!
|
||||
// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/.gitignore')!
|
||||
// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/build.sh')!
|
||||
// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/debug.sh')!
|
||||
// Add a directory and a file
|
||||
workspace.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker')!
|
||||
workspace.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/docker_ubuntu_install.sh')!
|
||||
println('selected (after add): ${workspace.selected_children()}')
|
||||
|
||||
// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/postgresql')!
|
||||
// Build a prompt from current selection (should be empty now)
|
||||
mut prompt := workspace.prompt(
|
||||
text: 'Using the selected files, i want you to get all print statments'
|
||||
)
|
||||
|
||||
// prompt := workspace1.prompt(
|
||||
// text: 'Using the selected files, i want you to get all print statments'
|
||||
// )
|
||||
println('--- PROMPT START ---')
|
||||
println(prompt)
|
||||
println('--- PROMPT END ---')
|
||||
|
||||
// println(prompt)
|
||||
// Remove the file by name, then the directory by name
|
||||
workspace.remove_file(name: 'docker_ubuntu_install.sh') or { println('remove_file: ${err}') }
|
||||
workspace.remove_dir(name: 'docker') or { println('remove_dir: ${err}') }
|
||||
println('selected (after remove): ${workspace.selected_children()}')
|
||||
|
||||
// List workspaces (names only)
|
||||
mut all := heroprompt.list_workspaces() or { []&heroprompt.Workspace{} }
|
||||
mut names := []string{}
|
||||
for w in all { names << w.name }
|
||||
println('workspaces: ${names}')
|
||||
|
||||
// Optionally delete the example workspace
|
||||
workspace.delete_workspace() or { println('delete_workspace: ${err}') }
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.core.playbook
|
||||
import os
|
||||
|
||||
|
||||
heroscript_config := '
|
||||
!!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib"
|
||||
'
|
||||
mut plbook := playbook.new(
|
||||
text: heroscript_config
|
||||
)!
|
||||
|
||||
|
||||
heroprompt.play(mut plbook)!
|
||||
@@ -4,6 +4,8 @@ import freeflowuniverse.herolib.core.base
|
||||
import freeflowuniverse.herolib.core.playbook { PlayBook }
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
__global (
|
||||
heroprompt_global map[string]&Workspace
|
||||
@@ -16,13 +18,32 @@ __global (
|
||||
pub struct ArgsGet {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
path string
|
||||
fromdb bool // will load from filesystem
|
||||
create bool // default will not create if not exist
|
||||
}
|
||||
|
||||
pub fn new(args ArgsGet) !&Workspace {
|
||||
// validate
|
||||
if args.name.len == 0 {
|
||||
return error('workspace name is required')
|
||||
}
|
||||
mut base_path := ''
|
||||
if args.path.len > 0 {
|
||||
if !os.exists(args.path) {
|
||||
return error('workspace path does not exist: ${args.path}')
|
||||
}
|
||||
if !os.is_dir(args.path) {
|
||||
return error('workspace path is not a directory: ${args.path}')
|
||||
}
|
||||
base_path = os.real_path(args.path)
|
||||
}
|
||||
mut obj := Workspace{
|
||||
name: args.name
|
||||
name: args.name
|
||||
base_path: base_path
|
||||
created: time.now()
|
||||
updated: time.now()
|
||||
is_saved: false
|
||||
}
|
||||
set(obj)!
|
||||
return get(name: args.name)!
|
||||
@@ -36,7 +57,7 @@ pub fn get(args ArgsGet) !&Workspace {
|
||||
if r.hexists('context:heroprompt', args.name)! {
|
||||
data := r.hget('context:heroprompt', args.name)!
|
||||
if data.len == 0 {
|
||||
return error('Workspace with name: heroprompt does not exist, prob bug.')
|
||||
return error('Workspace with name: ${args.name} does not exist, prob bug.')
|
||||
}
|
||||
mut obj := json.decode(Workspace, data)!
|
||||
set_in_mem(obj)!
|
||||
@@ -44,7 +65,7 @@ pub fn get(args ArgsGet) !&Workspace {
|
||||
if args.create {
|
||||
new(args)!
|
||||
} else {
|
||||
return error("Workspace with name 'heroprompt' does not exist")
|
||||
return error("Workspace with name '${args.name}' does not exist")
|
||||
}
|
||||
}
|
||||
return get(name: args.name)! // no longer from db nor create
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
module heroprompt
|
||||
|
||||
import freeflowuniverse.herolib.data.paramsparser
|
||||
import freeflowuniverse.herolib.data.encoderhero
|
||||
import os
|
||||
import time
|
||||
|
||||
pub const version = '0.0.0'
|
||||
const singleton = false
|
||||
const default = true
|
||||
|
||||
/
|
||||
// Workspace represents a workspace containing multiple directories
|
||||
// and their selected files for AI prompt generation
|
||||
@[heap]
|
||||
pub struct Workspace {
|
||||
pub mut:
|
||||
name string = 'default' // Workspace name
|
||||
base_path string // Base path of the workspace
|
||||
children []HeropromptChild // List of directories and files in this workspace
|
||||
base_path string // Base path of the workspace
|
||||
children []HeropromptChild // List of directories and files in this workspace
|
||||
created time.Time // Time of creation
|
||||
updated time.Time // Time of last update
|
||||
is_saved bool
|
||||
}
|
||||
|
||||
|
||||
// your checking & initialization code if needed
|
||||
fn obj_init(mycfg_ Workspace) !Workspace {
|
||||
return mycfg
|
||||
return mycfg_
|
||||
}
|
||||
|
||||
/////////////NORMALLY NO NEED TO TOUCH
|
||||
|
||||
@@ -4,32 +4,14 @@ import rand
|
||||
import time
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.develop.codewalker
|
||||
|
||||
|
||||
/
|
||||
/// Create a new workspace
|
||||
/// If the name is not passed, we will generate a random one
|
||||
fn (wsp Workspace) new(args_ NewWorkspaceParams) !&Workspace {
|
||||
mut args := args_
|
||||
if args.name.len == 0 {
|
||||
args.name = generate_random_workspace_name()
|
||||
}
|
||||
|
||||
// Validate and set base path
|
||||
if args.path.len > 0 {
|
||||
if !os.exists(args.path) {
|
||||
return error('Workspace path does not exist: ${args.path}')
|
||||
}
|
||||
if !os.is_dir(args.path) {
|
||||
return error('Workspace path is not a directory: ${args.path}')
|
||||
}
|
||||
}
|
||||
|
||||
mut workspace := &Workspace{
|
||||
name: args.name
|
||||
base_path: os.real_path(args.path)
|
||||
}
|
||||
return workspace
|
||||
fn (wsp &Workspace) save() !&Workspace {
|
||||
mut tmp := wsp
|
||||
tmp.updated = time.now()
|
||||
tmp.is_saved = true
|
||||
set(tmp)!
|
||||
return get(name: wsp.name)!
|
||||
}
|
||||
|
||||
// // WorkspaceItem represents a file or directory in the workspace tree
|
||||
@@ -209,13 +191,22 @@ pub mut:
|
||||
// add a directory to the selection (no recursion stored; recursion is done on-demand)
|
||||
pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
|
||||
if args.path.len == 0 {
|
||||
return error('The dir path is required')
|
||||
return error('the directory path is required')
|
||||
}
|
||||
|
||||
if !os.exists(args.path) || !os.is_dir(args.path) {
|
||||
return error('Path is not an existing directory: ${args.path}')
|
||||
return error('path is not an existing directory: ${args.path}')
|
||||
}
|
||||
|
||||
abs_path := os.real_path(args.path)
|
||||
name := os.base(abs_path)
|
||||
|
||||
for child in wsp.children {
|
||||
if child.name == name {
|
||||
return error('another directory with the same name already exists: ${name}')
|
||||
}
|
||||
}
|
||||
|
||||
mut ch := HeropromptChild{
|
||||
path: pathlib.Path{
|
||||
path: abs_path
|
||||
@@ -225,6 +216,7 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
|
||||
name: name
|
||||
}
|
||||
wsp.children << ch
|
||||
wsp.save()!
|
||||
return ch
|
||||
}
|
||||
|
||||
@@ -233,11 +225,24 @@ pub fn (mut wsp Workspace) add_file(args AddFileParams) !HeropromptChild {
|
||||
if args.path.len == 0 {
|
||||
return error('The file path is required')
|
||||
}
|
||||
|
||||
if !os.exists(args.path) || !os.is_file(args.path) {
|
||||
return error('Path is not an existing file: ${args.path}')
|
||||
}
|
||||
|
||||
abs_path := os.real_path(args.path)
|
||||
name := os.base(abs_path)
|
||||
|
||||
for child in wsp.children {
|
||||
if child.path.cat == .file && child.name == name {
|
||||
return error('another file with the same name already exists: ${name}')
|
||||
}
|
||||
|
||||
if child.path.cat == .dir && child.name == name {
|
||||
return error('${name}: is a directory, cannot add file with same name')
|
||||
}
|
||||
}
|
||||
|
||||
content := os.read_file(abs_path) or { '' }
|
||||
mut ch := HeropromptChild{
|
||||
path: pathlib.Path{
|
||||
@@ -248,10 +253,96 @@ pub fn (mut wsp Workspace) add_file(args AddFileParams) !HeropromptChild {
|
||||
name: name
|
||||
content: content
|
||||
}
|
||||
|
||||
wsp.children << ch
|
||||
wsp.save()!
|
||||
return ch
|
||||
}
|
||||
|
||||
// Removal API
|
||||
@[params]
|
||||
pub struct RemoveParams {
|
||||
pub mut:
|
||||
path string
|
||||
name string
|
||||
}
|
||||
|
||||
// Remove a directory from the selection (by absolute path or name)
|
||||
pub fn (mut wsp Workspace) remove_dir(args RemoveParams) ! {
|
||||
if args.path.len == 0 && args.name.len == 0 {
|
||||
return error('either path or name is required to remove a directory')
|
||||
}
|
||||
mut idxs := []int{}
|
||||
for i, ch in wsp.children {
|
||||
if ch.path.cat != .dir {
|
||||
continue
|
||||
}
|
||||
if args.path.len > 0 && os.real_path(args.path) == ch.path.path {
|
||||
idxs << i
|
||||
continue
|
||||
}
|
||||
if args.name.len > 0 && args.name == ch.name {
|
||||
idxs << i
|
||||
}
|
||||
}
|
||||
if idxs.len == 0 {
|
||||
return error('no matching directory found to remove')
|
||||
}
|
||||
// remove from end to start to keep indices valid
|
||||
idxs.sort(a > b)
|
||||
for i in idxs {
|
||||
wsp.children.delete(i)
|
||||
}
|
||||
wsp.save()!
|
||||
}
|
||||
|
||||
// Remove a file from the selection (by absolute path or name)
|
||||
pub fn (mut wsp Workspace) remove_file(args RemoveParams) ! {
|
||||
if args.path.len == 0 && args.name.len == 0 {
|
||||
return error('either path or name is required to remove a file')
|
||||
}
|
||||
mut idxs := []int{}
|
||||
for i, ch in wsp.children {
|
||||
if ch.path.cat != .file {
|
||||
continue
|
||||
}
|
||||
if args.path.len > 0 && os.real_path(args.path) == ch.path.path {
|
||||
idxs << i
|
||||
continue
|
||||
}
|
||||
if args.name.len > 0 && args.name == ch.name {
|
||||
idxs << i
|
||||
}
|
||||
}
|
||||
if idxs.len == 0 {
|
||||
return error('no matching file found to remove')
|
||||
}
|
||||
idxs.sort(a > b)
|
||||
for i in idxs {
|
||||
wsp.children.delete(i)
|
||||
}
|
||||
wsp.save()!
|
||||
}
|
||||
|
||||
// Delete this workspace from the store
|
||||
pub fn (wsp &Workspace) delete_workspace() ! {
|
||||
delete(name: wsp.name)!
|
||||
}
|
||||
|
||||
// List workspaces (wrapper over factory list)
|
||||
pub fn list_workspaces() ![]&Workspace {
|
||||
return list(fromdb: false)!
|
||||
}
|
||||
|
||||
pub fn list_workspaces_fromdb() ![]&Workspace {
|
||||
return list(fromdb: true)!
|
||||
}
|
||||
|
||||
// Get the currently selected children (copy)
|
||||
pub fn (wsp Workspace) selected_children() []HeropromptChild {
|
||||
return wsp.children.clone()
|
||||
}
|
||||
|
||||
// Build utilities
|
||||
fn list_files_recursive(root string) []string {
|
||||
mut out := []string{}
|
||||
@@ -268,7 +359,7 @@ fn list_files_recursive(root string) []string {
|
||||
}
|
||||
|
||||
// build_file_content generates formatted content for all selected files (and all files under selected dirs)
|
||||
fn (wsp Workspace) build_file_content() string {
|
||||
fn (wsp Workspace) build_file_content() !string {
|
||||
mut content := ''
|
||||
// files selected directly
|
||||
for ch in wsp.children {
|
||||
@@ -291,16 +382,18 @@ fn (wsp Workspace) build_file_content() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// files under selected directories
|
||||
// files under selected directories, using CodeWalker for filtered traversal
|
||||
for ch in wsp.children {
|
||||
if ch.path.cat == .dir {
|
||||
for f in list_files_recursive(ch.path.path) {
|
||||
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||
mut fm := cw.filemap_get(path: ch.path.path)!
|
||||
for rel, fc in fm.content {
|
||||
if content.len > 0 {
|
||||
content += '\n\n'
|
||||
}
|
||||
content += f + '\n'
|
||||
ext := get_file_extension(os.base(f))
|
||||
fc := os.read_file(f) or { '' }
|
||||
abs := os.join_path(ch.path.path, rel)
|
||||
content += abs + '\n'
|
||||
ext := get_file_extension(os.base(abs))
|
||||
if fc.len == 0 {
|
||||
content += '(Empty file)\n'
|
||||
} else {
|
||||
@@ -339,7 +432,7 @@ fn build_file_tree_fs(roots []HeropromptChild, prefix string) string {
|
||||
files.sort()
|
||||
// files
|
||||
for j, f in files {
|
||||
file_connector := if (j == files.len - 1 && dirs.len == 0) {
|
||||
file_connector := if j == files.len - 1 && dirs.len == 0 {
|
||||
'└── '
|
||||
} else {
|
||||
'├── '
|
||||
@@ -455,7 +548,7 @@ pub mut:
|
||||
pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
|
||||
user_instructions := wsp.build_user_instructions(args.text)
|
||||
file_map := wsp.build_file_map()
|
||||
file_contents := wsp.build_file_content()
|
||||
file_contents := wsp.build_file_content() or { '(Error building file contents)' }
|
||||
prompt := HeropromptTmpPrompt{
|
||||
user_instructions: user_instructions
|
||||
file_map: file_map
|
||||
|
||||
Reference in New Issue
Block a user