Skip to content
Closed
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