openclacky 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
# Skill management and execution
|
|
6
|
+
# Handles skill loading, command parsing, and subagent execution
|
|
7
|
+
module SkillManager
|
|
8
|
+
# Load all skills from configured locations
|
|
9
|
+
# @return [Array<Skill>]
|
|
10
|
+
def load_skills
|
|
11
|
+
@skill_loader.load_all
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Check if input is a skill command and process it
|
|
15
|
+
# @param input [String] User input
|
|
16
|
+
# @return [Hash, nil] Returns { skill: Skill, arguments: String } if skill command, nil otherwise
|
|
17
|
+
def parse_skill_command(input)
|
|
18
|
+
# Check for slash command pattern
|
|
19
|
+
if input.start_with?("/")
|
|
20
|
+
# Extract command and arguments
|
|
21
|
+
match = input.match(%r{^/(\S+)(?:\s+(.*))?$})
|
|
22
|
+
return nil unless match
|
|
23
|
+
|
|
24
|
+
skill_name = match[1]
|
|
25
|
+
arguments = match[2] || ""
|
|
26
|
+
|
|
27
|
+
# Find skill by command
|
|
28
|
+
skill = @skill_loader.find_by_command("/#{skill_name}")
|
|
29
|
+
return nil unless skill
|
|
30
|
+
|
|
31
|
+
# Check if user can invoke this skill
|
|
32
|
+
unless skill.user_invocable?
|
|
33
|
+
return nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
{ skill: skill, arguments: arguments }
|
|
37
|
+
else
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Execute a skill command
|
|
43
|
+
# @param input [String] User input (should be a skill command)
|
|
44
|
+
# @return [String] The expanded prompt with skill content
|
|
45
|
+
def execute_skill_command(input)
|
|
46
|
+
parsed = parse_skill_command(input)
|
|
47
|
+
return input unless parsed
|
|
48
|
+
|
|
49
|
+
skill = parsed[:skill]
|
|
50
|
+
arguments = parsed[:arguments]
|
|
51
|
+
|
|
52
|
+
# Check if skill requires forking a subagent
|
|
53
|
+
if skill.fork_agent?
|
|
54
|
+
return execute_skill_with_subagent(skill, arguments)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Process skill content with arguments (normal skill execution)
|
|
58
|
+
expanded_content = skill.process_content(arguments)
|
|
59
|
+
|
|
60
|
+
# Log skill usage
|
|
61
|
+
@ui&.log("Executing skill: #{skill.identifier}", level: :info)
|
|
62
|
+
|
|
63
|
+
expanded_content
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Generate skill context - loads all auto-invocable skills
|
|
67
|
+
# @return [String] Skill context to add to system prompt
|
|
68
|
+
def build_skill_context
|
|
69
|
+
# Load all auto-invocable skills
|
|
70
|
+
all_skills = @skill_loader.load_all
|
|
71
|
+
auto_invocable = all_skills.select(&:model_invocation_allowed?)
|
|
72
|
+
|
|
73
|
+
return "" if auto_invocable.empty?
|
|
74
|
+
|
|
75
|
+
context = "\n\n" + "=" * 80 + "\n"
|
|
76
|
+
context += "AVAILABLE SKILLS:\n"
|
|
77
|
+
context += "=" * 80 + "\n\n"
|
|
78
|
+
context += "CRITICAL SKILL USAGE RULES:\n"
|
|
79
|
+
context += "- When user's request matches a skill description, you MUST use invoke_skill tool\n"
|
|
80
|
+
context += "- NEVER implement skill functionality yourself - always delegate to the skill\n"
|
|
81
|
+
context += "- Example: invoke_skill(skill_name: 'code-explorer', task: 'Analyze project structure')\n\n"
|
|
82
|
+
context += "Available skills:\n\n"
|
|
83
|
+
|
|
84
|
+
auto_invocable.each do |skill|
|
|
85
|
+
context += "- name: #{skill.identifier}\n"
|
|
86
|
+
context += " description: #{skill.context_description}\n\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context += "\n"
|
|
90
|
+
context
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Execute a skill in a forked subagent
|
|
96
|
+
# @param skill [Skill] The skill to execute
|
|
97
|
+
# @param arguments [String] Arguments for the skill
|
|
98
|
+
# @return [String] Summary of subagent execution
|
|
99
|
+
def execute_skill_with_subagent(skill, arguments)
|
|
100
|
+
# Log subagent fork
|
|
101
|
+
@ui&.show_info("Subagent start: #{skill.identifier}")
|
|
102
|
+
|
|
103
|
+
# Build task content from skill
|
|
104
|
+
task_content = skill.process_content(arguments)
|
|
105
|
+
|
|
106
|
+
# Fork subagent with skill configuration
|
|
107
|
+
subagent = fork_subagent(
|
|
108
|
+
model: skill.subagent_model,
|
|
109
|
+
forbidden_tools: skill.forbidden_tools_list,
|
|
110
|
+
system_prompt_suffix: task_content
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Run subagent
|
|
114
|
+
result = subagent.run(arguments)
|
|
115
|
+
|
|
116
|
+
# Generate summary
|
|
117
|
+
summary = generate_subagent_summary(subagent)
|
|
118
|
+
|
|
119
|
+
# Insert summary back to parent agent messages (replacing the instruction message)
|
|
120
|
+
# Find and replace the last message with subagent_instructions flag
|
|
121
|
+
messages_with_instructions = @messages.select { |m| m[:subagent_instructions] }
|
|
122
|
+
if messages_with_instructions.any?
|
|
123
|
+
instruction_msg = messages_with_instructions.last
|
|
124
|
+
instruction_msg[:content] = summary
|
|
125
|
+
instruction_msg.delete(:subagent_instructions)
|
|
126
|
+
instruction_msg[:subagent_result] = true
|
|
127
|
+
instruction_msg[:skill_name] = skill.identifier
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Log completion
|
|
131
|
+
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{result[:total_cost_usd].round(4)}")
|
|
132
|
+
|
|
133
|
+
# Return summary as the skill execution result
|
|
134
|
+
summary
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
# System prompt construction
|
|
6
|
+
# Builds system prompt with project rules and skill context
|
|
7
|
+
module SystemPromptBuilder
|
|
8
|
+
# System prompt for the coding agent
|
|
9
|
+
SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
10
|
+
You are OpenClacky, an AI coding assistant and technical co-founder, designed to help non-technical
|
|
11
|
+
users complete software development projects. You are responsible for development in the current project.
|
|
12
|
+
|
|
13
|
+
Your role is to:
|
|
14
|
+
- Understand project requirements and translate them into technical solutions
|
|
15
|
+
- Write clean, maintainable, and well-documented code
|
|
16
|
+
- Follow best practices and industry standards
|
|
17
|
+
- Explain technical concepts in simple terms when needed
|
|
18
|
+
- Proactively identify potential issues and suggest improvements
|
|
19
|
+
- Help with debugging, testing, and deployment
|
|
20
|
+
|
|
21
|
+
Working process:
|
|
22
|
+
1. **For complex tasks with multiple steps**:
|
|
23
|
+
- Use todo_manager to create a complete TODO list FIRST
|
|
24
|
+
- After creating the TODO list, START EXECUTING each task immediately
|
|
25
|
+
- Don't stop after planning - continue to work on the tasks!
|
|
26
|
+
2. Always read existing code before making changes (use file_reader/glob/grep or invoke code-explorer skill)
|
|
27
|
+
3. Ask clarifying questions if requirements are unclear
|
|
28
|
+
4. Break down complex tasks into manageable steps
|
|
29
|
+
5. **USE TOOLS to create/modify files** - don't just return code
|
|
30
|
+
6. Write code that is secure, efficient, and easy to understand
|
|
31
|
+
7. Test your changes using the shell tool when appropriate
|
|
32
|
+
8. **IMPORTANT**: After completing each step, mark the TODO as completed and continue to the next one
|
|
33
|
+
9. Keep working until ALL TODOs are completed or you need user input
|
|
34
|
+
10. Provide brief explanations after completing actions
|
|
35
|
+
|
|
36
|
+
IMPORTANT: You should frequently refer to the existing codebase. For unclear instructions,
|
|
37
|
+
prioritize understanding the codebase first before answering or taking action.
|
|
38
|
+
Always read relevant code files to understand the project structure, patterns, and conventions.
|
|
39
|
+
|
|
40
|
+
CRITICAL RULE FOR TODO MANAGER:
|
|
41
|
+
When using todo_manager to add tasks, you MUST continue working immediately after adding ALL todos.
|
|
42
|
+
Adding todos is NOT completion - it's just the planning phase!
|
|
43
|
+
Workflow: add todo 1 → add todo 2 → add todo 3 → START WORKING on todo 1 → complete(1) → work on todo 2 → complete(2) → etc.
|
|
44
|
+
NEVER stop after just adding todos without executing them!
|
|
45
|
+
|
|
46
|
+
NOTE: Available skills are listed below in the AVAILABLE SKILLS section.
|
|
47
|
+
When a user's request matches a skill, you MUST use the skill tool instead of implementing it yourself.
|
|
48
|
+
PROMPT
|
|
49
|
+
|
|
50
|
+
# Build complete system prompt with project rules and skills
|
|
51
|
+
# @return [String] Complete system prompt
|
|
52
|
+
def build_system_prompt
|
|
53
|
+
prompt = SYSTEM_PROMPT.dup
|
|
54
|
+
|
|
55
|
+
# Try to load project rules from multiple sources (in order of priority)
|
|
56
|
+
rules_files = [
|
|
57
|
+
{ path: ".clackyrules", name: ".clackyrules" },
|
|
58
|
+
{ path: ".cursorrules", name: ".cursorrules" },
|
|
59
|
+
{ path: "CLAUDE.md", name: "CLAUDE.md" }
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
rules_content = nil
|
|
63
|
+
rules_source = nil
|
|
64
|
+
|
|
65
|
+
rules_files.each do |file_info|
|
|
66
|
+
full_path = File.join(@working_dir, file_info[:path])
|
|
67
|
+
if File.exist?(full_path)
|
|
68
|
+
content = File.read(full_path).strip
|
|
69
|
+
unless content.empty?
|
|
70
|
+
rules_content = content
|
|
71
|
+
rules_source = file_info[:name]
|
|
72
|
+
break
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Add rules to prompt if found
|
|
78
|
+
if rules_content && rules_source
|
|
79
|
+
prompt += "\n\n" + "=" * 80 + "\n"
|
|
80
|
+
prompt += "PROJECT-SPECIFIC RULES (from #{rules_source}):\n"
|
|
81
|
+
prompt += "=" * 80 + "\n"
|
|
82
|
+
prompt += rules_content
|
|
83
|
+
prompt += "\n" + "=" * 80 + "\n"
|
|
84
|
+
prompt += "⚠️ IMPORTANT: Follow these project-specific rules at all times!\n"
|
|
85
|
+
prompt += "=" * 80
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Add all loaded skills to system prompt
|
|
89
|
+
skill_context = build_skill_context
|
|
90
|
+
prompt += skill_context if skill_context && !skill_context.empty?
|
|
91
|
+
|
|
92
|
+
prompt
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
# Time Machine module for task history management with undo/redo support
|
|
6
|
+
# Stores complete file snapshots (AFTER state) to support message compression
|
|
7
|
+
module TimeMachine
|
|
8
|
+
# Initialize Time Machine state
|
|
9
|
+
private def init_time_machine
|
|
10
|
+
@task_parents ||= {} # { task_id => parent_id }
|
|
11
|
+
@current_task_id ||= 0 # Latest created task ID
|
|
12
|
+
@active_task_id ||= 0 # Current active task ID (for undo/redo)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Start a new task and establish parent relationship
|
|
16
|
+
# Made public for testing
|
|
17
|
+
def start_new_task
|
|
18
|
+
parent_id = @active_task_id
|
|
19
|
+
@current_task_id += 1
|
|
20
|
+
@active_task_id = @current_task_id
|
|
21
|
+
@task_parents[@current_task_id] = parent_id
|
|
22
|
+
|
|
23
|
+
@current_task_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Save snapshots of modified files (AFTER state)
|
|
27
|
+
# @param modified_files [Array<String>] List of file paths that were modified
|
|
28
|
+
# Made public for testing
|
|
29
|
+
def save_modified_files_snapshot(modified_files)
|
|
30
|
+
return if modified_files.nil? || modified_files.empty?
|
|
31
|
+
|
|
32
|
+
snapshot_dir = File.join(
|
|
33
|
+
Dir.home,
|
|
34
|
+
".clacky",
|
|
35
|
+
"snapshots",
|
|
36
|
+
@session_id,
|
|
37
|
+
"task-#{@current_task_id}"
|
|
38
|
+
)
|
|
39
|
+
FileUtils.mkdir_p(snapshot_dir)
|
|
40
|
+
|
|
41
|
+
modified_files.each do |file_path|
|
|
42
|
+
next unless File.exist?(file_path)
|
|
43
|
+
|
|
44
|
+
# Save file content to snapshot
|
|
45
|
+
relative_path = file_path.start_with?(@working_dir) ?
|
|
46
|
+
file_path.sub(@working_dir + "/", "") : File.basename(file_path)
|
|
47
|
+
|
|
48
|
+
snapshot_file = File.join(snapshot_dir, relative_path)
|
|
49
|
+
FileUtils.mkdir_p(File.dirname(snapshot_file))
|
|
50
|
+
FileUtils.cp(file_path, snapshot_file)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
# Silently handle errors in tests
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Restore files to the state at given task
|
|
57
|
+
# @param task_id [Integer] Target task ID
|
|
58
|
+
# Made public for testing
|
|
59
|
+
def restore_to_task_state(task_id)
|
|
60
|
+
# Collect all modified files from task 1 to target task
|
|
61
|
+
files_to_restore = {}
|
|
62
|
+
|
|
63
|
+
(1..task_id).each do |tid|
|
|
64
|
+
snapshot_dir = File.join(
|
|
65
|
+
Dir.home,
|
|
66
|
+
".clacky",
|
|
67
|
+
"snapshots",
|
|
68
|
+
@session_id,
|
|
69
|
+
"task-#{tid}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
next unless Dir.exist?(snapshot_dir)
|
|
73
|
+
|
|
74
|
+
Dir.glob(File.join(snapshot_dir, "**", "*")).each do |snapshot_file|
|
|
75
|
+
next if File.directory?(snapshot_file)
|
|
76
|
+
|
|
77
|
+
relative_path = snapshot_file.sub(snapshot_dir + "/", "")
|
|
78
|
+
files_to_restore[relative_path] = snapshot_file
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Restore files
|
|
83
|
+
files_to_restore.each do |relative_path, snapshot_file|
|
|
84
|
+
target_file = File.join(@working_dir, relative_path)
|
|
85
|
+
FileUtils.mkdir_p(File.dirname(target_file))
|
|
86
|
+
FileUtils.cp(snapshot_file, target_file)
|
|
87
|
+
end
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
# Silently handle errors in tests
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Filter messages to only show tasks up to active_task_id
|
|
94
|
+
# This hides "future" messages when user has undone
|
|
95
|
+
# Made public for testing
|
|
96
|
+
def active_messages
|
|
97
|
+
return @messages if @active_task_id == @current_task_id
|
|
98
|
+
|
|
99
|
+
@messages.select do |msg|
|
|
100
|
+
msg_task_id = msg[:task_id] || 0
|
|
101
|
+
msg_task_id <= @active_task_id
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Undo to parent task
|
|
106
|
+
def undo_last_task
|
|
107
|
+
parent_id = @task_parents[@active_task_id]
|
|
108
|
+
return { success: false, message: "Already at root task" } if parent_id.nil? || parent_id == 0
|
|
109
|
+
|
|
110
|
+
restore_to_task_state(parent_id)
|
|
111
|
+
@active_task_id = parent_id
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
success: true,
|
|
115
|
+
message: "⏪ Undone to task #{parent_id}",
|
|
116
|
+
task_id: parent_id
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Switch to specific task (for redo or branch switching)
|
|
121
|
+
def switch_to_task(target_task_id)
|
|
122
|
+
if target_task_id > @current_task_id || target_task_id < 1
|
|
123
|
+
return { success: false, message: "Invalid task ID: #{target_task_id}" }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
restore_to_task_state(target_task_id)
|
|
127
|
+
@active_task_id = target_task_id
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
success: true,
|
|
131
|
+
message: "⏩ Switched to task #{target_task_id}",
|
|
132
|
+
task_id: target_task_id
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get children of a task (for branch detection)
|
|
137
|
+
def get_child_tasks(task_id)
|
|
138
|
+
@task_parents.select { |_, parent| parent == task_id }.keys
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get task history with summaries for UI display
|
|
142
|
+
# @param limit [Integer] Maximum number of recent tasks to return
|
|
143
|
+
# @return [Array<Hash>] Task history with metadata
|
|
144
|
+
def get_task_history(limit: 10)
|
|
145
|
+
return [] if @current_task_id == 0
|
|
146
|
+
|
|
147
|
+
tasks = []
|
|
148
|
+
(1..@current_task_id).to_a.reverse.take(limit).reverse.each do |task_id|
|
|
149
|
+
# Find first user message for this task
|
|
150
|
+
first_user_msg = @messages.find do |msg|
|
|
151
|
+
msg[:task_id] == task_id && msg[:role] == "user"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
summary = if first_user_msg
|
|
155
|
+
content = extract_message_text(first_user_msg[:content])
|
|
156
|
+
# Truncate to 60 characters (including "...")
|
|
157
|
+
content.length > 60 ? "#{content[0...57]}..." : content
|
|
158
|
+
else
|
|
159
|
+
"Task #{task_id}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Determine task status
|
|
163
|
+
status = if task_id == @active_task_id
|
|
164
|
+
:current
|
|
165
|
+
elsif task_id < @active_task_id
|
|
166
|
+
:past
|
|
167
|
+
else
|
|
168
|
+
:future
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if task has branches (multiple children)
|
|
172
|
+
children = get_child_tasks(task_id)
|
|
173
|
+
has_branches = children.length > 1
|
|
174
|
+
|
|
175
|
+
tasks << {
|
|
176
|
+
task_id: task_id,
|
|
177
|
+
summary: summary,
|
|
178
|
+
status: status,
|
|
179
|
+
has_branches: has_branches
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
tasks
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Extract text from message content (handles both string and array formats)
|
|
187
|
+
private def extract_message_text(content)
|
|
188
|
+
if content.is_a?(String)
|
|
189
|
+
content
|
|
190
|
+
elsif content.is_a?(Array)
|
|
191
|
+
text_parts = content.select { |part| part[:type] == "text" }
|
|
192
|
+
text_parts.map { |part| part[:text] }.join(" ")
|
|
193
|
+
else
|
|
194
|
+
""
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|