Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions chatapps/base/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ type RichContent struct {
Blocks []any
Embeds []any
Attachments []Attachment
Reactions []Reaction
}

// Reaction represents a reaction to add to a message
type Reaction struct {
Name string // emoji name (e.g., "thumbsup", "+1")
Channel string
Timestamp string // message timestamp to react to
}

type Attachment struct {
Expand Down
34 changes: 32 additions & 2 deletions chatapps/configs/slack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,43 @@ platform: slack
provider:
# AI provider type: claude-code, opencode
type: claude-code

# Model to use: sonnet, haiku, opus, etc.
default_model: sonnet

# Permission mode: bypass-permissions, ask, etc.
default_permission_mode: bypass-permissions

# -----------------------------------------------------------------------------
# Engine Configuration
# -----------------------------------------------------------------------------
#
engine:
# Working directory for the AI agent
# This is where the agent will operate, read files, and make changes.
#
# For HotPlex development, use the source code directory.
# For other use cases, you can specify a different path.
#
# TYPE: string (file path)
# DEFAULT: /tmp/hotplex-chatapps/slack/<session_id>
#
# Examples:
# work_dir: /Users/you/HotPlex # HotPlex source code
# work_dir: /home/user/projects/myapp # Your project directory
# work_dir: . # Current directory (hotplexd startup dir)
work_dir: .

# Execution timeout (how long to wait for AI response)
# TYPE: duration
# DEFAULT: 30m
# timeout: 30m

# Idle timeout (how long to keep session alive without activity)
# TYPE: duration
# DEFAULT: 30m
# idle_timeout: 30m

# -----------------------------------------------------------------------------
# Connection Mode
# -----------------------------------------------------------------------------
Expand Down
42 changes: 33 additions & 9 deletions chatapps/engine_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,19 @@ type StreamCallback struct {
logger *slog.Logger
mu sync.Mutex
isFirst bool
metadata map[string]any // Original message metadata (channel_id, thread_ts, etc.)
}

// NewStreamCallback creates a new StreamCallback
func NewStreamCallback(ctx context.Context, sessionID, platform string, adapters *AdapterManager, logger *slog.Logger) *StreamCallback {
func NewStreamCallback(ctx context.Context, sessionID, platform string, adapters *AdapterManager, logger *slog.Logger, metadata map[string]any) *StreamCallback {
return &StreamCallback{
ctx: ctx,
sessionID: sessionID,
platform: platform,
adapters: adapters,
logger: logger,
isFirst: true,
metadata: metadata,
}
}

Expand Down Expand Up @@ -231,14 +233,30 @@ func (c *StreamCallback) sendMessage(content string, eventType string) error {
return nil
}

// Build metadata with original message's platform-specific data (channel_id, thread_ts, etc.)
metadata := map[string]any{
"stream": true,
"event_type": eventType,
}

// Copy important metadata from original message
if c.metadata != nil {
if channelID, ok := c.metadata["channel_id"]; ok {
metadata["channel_id"] = channelID
}
if channelType, ok := c.metadata["channel_type"]; ok {
metadata["channel_type"] = channelType
}
if threadTS, ok := c.metadata["thread_ts"]; ok {
metadata["thread_ts"] = threadTS
}
}

msg := &ChatMessage{
Platform: c.platform,
SessionID: c.sessionID,
Content: content,
Metadata: map[string]any{
"stream": true,
"event_type": eventType,
},
Metadata: metadata,
}

return c.adapters.SendMessage(c.ctx, c.platform, c.sessionID, msg)
Expand Down Expand Up @@ -300,13 +318,19 @@ func WithConfigLoader(loader *ConfigLoader) EngineMessageHandlerOption {
// Handle implements MessageHandler
func (h *EngineMessageHandler) Handle(ctx context.Context, msg *ChatMessage) error {
// Determine work directory
workDir := h.workDirFn(msg.SessionID)
workDir := ""
if h.workDirFn != nil {
workDir = h.workDirFn(msg.SessionID)
}
if workDir == "" {
workDir = "/tmp/hotplex-chatapps"
}

// Determine task instructions
taskInstr := h.taskInstrFn(msg.SessionID)
taskInstr := ""
if h.taskInstrFn != nil {
taskInstr = h.taskInstrFn(msg.SessionID)
}
if taskInstr == "" && h.configLoader != nil {
taskInstr = h.configLoader.GetTaskInstructions(msg.Platform)
}
Expand Down Expand Up @@ -336,8 +360,8 @@ func (h *EngineMessageHandler) Handle(ctx context.Context, msg *ChatMessage) err
TaskInstructions: fullInstructions,
}

// Create stream callback
callback := NewStreamCallback(ctx, msg.SessionID, msg.Platform, h.adapters, h.logger)
// Create stream callback with original message metadata
callback := NewStreamCallback(ctx, msg.SessionID, msg.Platform, h.adapters, h.logger, msg.Metadata)
wrappedCallback := func(eventType string, data any) error {
return callback.Handle(eventType, data)
}
Expand Down
38 changes: 38 additions & 0 deletions chatapps/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ func setupPlatform(
WithConfigLoader(loader),
WithLogger(logger),
WithWorkDirFn(func(sessionID string) string {
// Use work_dir from config if specified
if pc.Engine.WorkDir != "" {
// Expand ~ to home directory
workDir := expandPath(pc.Engine.WorkDir)
return workDir
}
// Default: use temp directory with platform/session isolation
return filepath.Join("/tmp/hotplex-chatapps", platform, sessionID)
}),
)
Expand Down Expand Up @@ -181,3 +188,34 @@ func createEngineForPlatform(pc *PlatformConfig, logger *slog.Logger) (*engine.E

return engine.NewEngine(opts)
}

// expandPath expands ~ to the user's home directory and cleans the path.
// Supports both ~ and ~/path formats.
func expandPath(path string) string {
if len(path) == 0 {
return path
}

// Handle ~ expansion
if path[0] == '~' {
homeDir, err := os.UserHomeDir()
if err != nil {
return path // Return original path if home dir cannot be determined
}

if len(path) == 1 {
return homeDir
}

// Handle ~/path
if path[1] == '/' || path[1] == filepath.Separator {
return filepath.Join(homeDir, path[2:])
}

// Handle ~username/path (not commonly used, but supported)
return filepath.Join(homeDir, path[1:])
}

// Clean the path to resolve any . or .. elements
return filepath.Clean(path)
}
Loading