style: fix format issues (#255)

This commit is contained in:
Carlos Florêncio
2026-02-19 22:18:01 +00:00
committed by GitHub
parent fe9298a7ad
commit 61ef224ec7
753 changed files with 35129 additions and 21169 deletions
+33
View File
@@ -45,6 +45,39 @@ jobs:
- name: Build
run: go build -v ./cmd/kandev
- name: Check gofmt
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
else
BASE_SHA="${{ github.event.before }}"
fi
if [[ -z "${BASE_SHA}" || "${BASE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
BASE_SHA="$(git rev-list --max-parents=0 HEAD)"
fi
CHANGED_GO_FILES="$(
git diff --name-only --diff-filter=ACMRT "${BASE_SHA}"..."${{ github.sha }}" -- apps/backend \
| grep -E '^apps/backend/.*\.go$' \
|| true
)"
if [[ -z "${CHANGED_GO_FILES}" ]]; then
echo "No backend Go files changed."
exit 0
fi
UNFORMATTED="$(echo "${CHANGED_GO_FILES}" | sed 's#^apps/backend/##' | xargs gofmt -l)"
if [[ -n "${UNFORMATTED}" ]]; then
echo "The following changed Go files are not formatted with gofmt:"
echo "${UNFORMATTED}"
exit 1
fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
+40
View File
@@ -4,6 +4,10 @@ on:
push:
branches: [main]
paths:
- "apps/cli/**"
- "apps/package.json"
- "apps/prettier.config.mjs"
- "apps/.prettierignore"
- "apps/web/**"
- "apps/packages/**"
- "apps/pnpm-lock.yaml"
@@ -11,6 +15,10 @@ on:
pull_request:
branches: [main]
paths:
- "apps/cli/**"
- "apps/package.json"
- "apps/prettier.config.mjs"
- "apps/.prettierignore"
- "apps/web/**"
- "apps/packages/**"
- "apps/pnpm-lock.yaml"
@@ -41,6 +49,38 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
else
BASE_SHA="${{ github.event.before }}"
fi
if [[ -z "${BASE_SHA}" || "${BASE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
BASE_SHA="$(git rev-list --max-parents=0 HEAD)"
fi
CHANGED_FILES="$(
git diff --name-only --diff-filter=ACMRT "${BASE_SHA}"..."${{ github.sha }}" \
| grep -E '^apps/(web|cli|packages)/.*\.(ts|tsx|js|jsx|mjs|cjs|json|md|mdx|yml|yaml|css|scss)$' \
|| true
)"
if [[ -z "${CHANGED_FILES}" ]]; then
echo "No Prettier-scoped files changed."
exit 0
fi
echo "Checking Prettier on changed files:"
echo "${CHANGED_FILES}"
echo "${CHANGED_FILES}" \
| sed 's#^apps/##' \
| xargs pnpm --dir apps exec prettier --check --ignore-unknown
- name: Run lint
run: pnpm --filter @kandev/web lint
+1
View File
@@ -76,6 +76,7 @@ apps/backend/mock-agent
.next/
out/
node_modules/
.pnpm-store/
# TypeScript
*.tsbuildinfo
+14
View File
@@ -2,6 +2,13 @@ repos:
# Backend (Go) hooks
- repo: local
hooks:
- id: gofmt
name: gofmt (backend)
entry: bash -c 'gofmt -w "$@"' --
language: system
files: ^apps/backend/.*\.go$
pass_filenames: true
- id: go-tests
name: Go tests (backend)
entry: bash -c 'cd apps/backend && go test ./...'
@@ -19,6 +26,13 @@ repos:
# Web app (TypeScript/React) hooks
- repo: local
hooks:
- id: prettier-format
name: Prettier format (web/cli/packages)
entry: bash -c 'cd apps && pnpm exec prettier --write --ignore-unknown "${@/#apps\//}"' --
language: system
files: ^apps/(web|cli|packages)/.*\.(ts|tsx|js|jsx|mjs|cjs|json|md|mdx|yml|yaml|css|scss)$
pass_filenames: true
- id: web-lint
name: Web lint
entry: bash -c 'cd apps && pnpm --filter @kandev/web lint'
+8 -2
View File
@@ -82,9 +82,10 @@ help:
@echo " lint Run linters for both components"
@echo " lint-backend Run Go linters"
@echo " lint-web Run ESLint"
@echo " lint-format Check formatting with Prettier (web/cli/packages)"
@echo " fmt Format all code"
@echo " fmt-backend Format Go code"
@echo " fmt-web Format web code with ESLint"
@echo " fmt-web Format web/cli/packages with Prettier, then ESLint --fix (web)"
@echo ""
@echo "Cleanup:"
@echo " clean Remove all build artifacts"
@@ -220,6 +221,11 @@ lint-web:
@printf "$(CYAN)Linting web app...$(RESET)\n"
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web lint
.PHONY: lint-format
lint-format:
@printf "$(CYAN)Checking formatting...$(RESET)\n"
@cd $(APPS_DIR) && $(PNPM) run format:check
.PHONY: fmt
fmt: fmt-backend fmt-web
@printf "\n$(GREEN)$(BOLD)✓ Code formatting complete!$(RESET)\n"
@@ -232,7 +238,7 @@ fmt-backend:
.PHONY: fmt-web
fmt-web:
@printf "$(CYAN)Formatting web code...$(RESET)\n"
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web lint -- --fix || true
@cd $(APPS_DIR) && $(PNPM) run format
.PHONY: typecheck-web
typecheck-web:
+5
View File
@@ -0,0 +1,5 @@
node_modules
.next
dist
build
coverage
+1 -1
View File
@@ -12,9 +12,9 @@ import (
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/common/scripts"
"github.com/kandev/kandev/internal/events"
lspinstaller "github.com/kandev/kandev/internal/lsp/installer"
"github.com/kandev/kandev/internal/events/bus"
gateways "github.com/kandev/kandev/internal/gateway/websocket"
lspinstaller "github.com/kandev/kandev/internal/lsp/installer"
notificationcontroller "github.com/kandev/kandev/internal/notifications/controller"
notificationservice "github.com/kandev/kandev/internal/notifications/service"
notificationstore "github.com/kandev/kandev/internal/notifications/store"
+3 -3
View File
@@ -9,12 +9,12 @@ import (
"go.uber.org/zap"
"github.com/kandev/kandev/internal/agent/docker"
"github.com/kandev/kandev/internal/agent/lifecycle"
agenthandlers "github.com/kandev/kandev/internal/agent/handlers"
"github.com/kandev/kandev/internal/agent/lifecycle"
agentsettingscontroller "github.com/kandev/kandev/internal/agent/settings/controller"
agentsettingshandlers "github.com/kandev/kandev/internal/agent/settings/handlers"
analyticsrepository "github.com/kandev/kandev/internal/analytics/repository"
analyticshandlers "github.com/kandev/kandev/internal/analytics/handlers"
analyticsrepository "github.com/kandev/kandev/internal/analytics/repository"
"github.com/kandev/kandev/internal/clarification"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/common/logger"
@@ -29,9 +29,9 @@ import (
"github.com/kandev/kandev/internal/orchestrator"
promptcontroller "github.com/kandev/kandev/internal/prompts/controller"
prompthandlers "github.com/kandev/kandev/internal/prompts/handlers"
taskhandlers "github.com/kandev/kandev/internal/task/handlers"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/repository"
taskhandlers "github.com/kandev/kandev/internal/task/handlers"
taskservice "github.com/kandev/kandev/internal/task/service"
usercontroller "github.com/kandev/kandev/internal/user/controller"
userhandlers "github.com/kandev/kandev/internal/user/handlers"
+1 -1
View File
@@ -5,9 +5,9 @@ import (
"github.com/kandev/kandev/internal/agent/lifecycle"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/common/constants"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/task/models"
taskservice "github.com/kandev/kandev/internal/task/service"
"github.com/kandev/kandev/internal/worktree"
@@ -55,11 +55,11 @@ func TestParseModelFromArgs(t *testing.T) {
func TestDelayRange(t *testing.T) {
tests := []struct {
model string
wantMinLo int
wantMinHi int
wantMaxLo int
wantMaxHi int
model string
wantMinLo int
wantMinHi int
wantMaxLo int
wantMaxHi int
}{
{"mock-fast", 10, 10, 50, 50},
{"mock-slow", 500, 500, 3000, 3000},
+24 -24
View File
@@ -22,8 +22,8 @@ const (
// Tool names matching Claude Code conventions
const (
ToolBash = "Bash"
ToolEdit = "Edit"
ToolBash = "Bash"
ToolEdit = "Edit"
ToolRead = "Read"
ToolGlob = "Glob"
ToolGrep = "Grep"
@@ -69,8 +69,8 @@ type SystemMsg struct {
// AssistantMsg is an assistant message with content blocks.
type AssistantMsg struct {
Type string `json:"type"`
ParentToolUseID string `json:"parent_tool_use_id,omitempty"`
Type string `json:"type"`
ParentToolUseID string `json:"parent_tool_use_id,omitempty"`
Message AssistantBody `json:"message"`
}
@@ -114,9 +114,9 @@ type Usage struct {
// UserMsg is a user message (used for tool results).
type UserMsg struct {
Type string `json:"type"`
ParentToolUseID string `json:"parent_tool_use_id,omitempty"`
Message UserMsgBody `json:"message"`
Type string `json:"type"`
ParentToolUseID string `json:"parent_tool_use_id,omitempty"`
Message UserMsgBody `json:"message"`
}
// UserMsgBody is the body of a user message with tool results.
@@ -127,15 +127,15 @@ type UserMsgBody struct {
// ResultMsg is the final result message for a turn.
type ResultMsg struct {
Type string `json:"type"`
Result json.RawMessage `json:"result"`
CostUSD float64 `json:"cost_usd"`
DurationMS int64 `json:"duration_ms"`
DurationAPIMS int64 `json:"duration_api_ms"`
IsError bool `json:"is_error"`
NumTurns int `json:"num_turns"`
TotalInputTokens int64 `json:"total_input_tokens"`
TotalOutputTokens int64 `json:"total_output_tokens"`
Type string `json:"type"`
Result json.RawMessage `json:"result"`
CostUSD float64 `json:"cost_usd"`
DurationMS int64 `json:"duration_ms"`
DurationAPIMS int64 `json:"duration_api_ms"`
IsError bool `json:"is_error"`
NumTurns int `json:"num_turns"`
TotalInputTokens int64 `json:"total_input_tokens"`
TotalOutputTokens int64 `json:"total_output_tokens"`
ModelUsage map[string]ModelUsageStats `json:"model_usage,omitempty"`
}
@@ -152,9 +152,9 @@ type ResultData struct {
// ControlRequestMsg is a control request emitted to stdout (permission requests).
type ControlRequestMsg struct {
Type string `json:"type"`
RequestID string `json:"request_id"`
Request ControlRequestBody `json:"request"`
Type string `json:"type"`
RequestID string `json:"request_id"`
Request ControlRequestBody `json:"request"`
}
// ControlRequestBody is the body of a control request.
@@ -167,15 +167,15 @@ type ControlRequestBody struct {
// ControlResponseMsg is a control response emitted to stdout (for initialize).
type ControlResponseMsg struct {
Type string `json:"type"`
Response ControlResponseBody `json:"response"`
Type string `json:"type"`
Response ControlResponseBody `json:"response"`
}
// ControlResponseBody is the body of a control response.
type ControlResponseBody struct {
Subtype string `json:"subtype"`
RequestID string `json:"request_id"`
Response *InitializeResponse `json:"response,omitempty"`
Subtype string `json:"subtype"`
RequestID string `json:"request_id"`
Response *InitializeResponse `json:"response,omitempty"`
}
// InitializeResponse is the response to an initialize control request.
+2 -2
View File
@@ -46,8 +46,8 @@ func (a *Amp) DisplayName() string { return "Amp" }
func (a *Amp) Description() string {
return "Sourcegraph Amp CLI-powered autonomous coding agent using stream-json protocol."
}
func (a *Amp) Enabled() bool { return true }
func (a *Amp) DisplayOrder() int { return 7 }
func (a *Amp) Enabled() bool { return true }
func (a *Amp) DisplayOrder() int { return 7 }
func (a *Amp) Logo(v LogoVariant) []byte {
if v == LogoDark {
+6 -6
View File
@@ -50,8 +50,8 @@ func (a *Auggie) ID() string { return "auggie" }
func (a *Auggie) Name() string { return "Augment Coding Agent" }
func (a *Auggie) DisplayName() string { return "Auggie" }
func (a *Auggie) Description() string { return "Auggie CLI-powered autonomous coding agent." }
func (a *Auggie) Enabled() bool { return true }
func (a *Auggie) DisplayOrder() int { return 3 }
func (a *Auggie) Enabled() bool { return true }
func (a *Auggie) DisplayOrder() int { return 3 }
func (a *Auggie) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -100,10 +100,10 @@ func (a *Auggie) BuildCommand(opts CommandOptions) Command {
func (a *Auggie) Runtime() *RuntimeConfig {
canRecover := false
return &RuntimeConfig{
Image: "kandev/multi-agent",
Tag: "latest",
Cmd: Cmd("npx", "-y", "@augmentcode/auggie@0.15.0", "--acp").Build(),
WorkingDir: "/workspace",
Image: "kandev/multi-agent",
Tag: "latest",
Cmd: Cmd("npx", "-y", "@augmentcode/auggie@0.15.0", "--acp").Build(),
WorkingDir: "/workspace",
RequiredEnv: []string{"AUGMENT_SESSION_AUTH"},
Env: map[string]string{},
Mounts: []MountTemplate{
@@ -25,12 +25,12 @@ func assertDefaultModel(t *testing.T, models []Model, wantDef string) {
func TestAuggieParseModels(t *testing.T) {
tests := []struct {
name string
input string
wantLen int
wantErr bool
wantIDs []string
wantDef string // expected default model ID
name string
input string
wantLen int
wantErr bool
wantIDs []string
wantDef string // expected default model ID
}{
{
name: "normal output with descriptions",
@@ -48,8 +48,8 @@ func (a *ClaudeCode) DisplayName() string { return "Claude" }
func (a *ClaudeCode) Description() string {
return "Anthropic Claude Code CLI-powered autonomous coding agent using the stream-json protocol."
}
func (a *ClaudeCode) Enabled() bool { return true }
func (a *ClaudeCode) DisplayOrder() int { return 1 }
func (a *ClaudeCode) Enabled() bool { return true }
func (a *ClaudeCode) DisplayOrder() int { return 1 }
func (a *ClaudeCode) Logo(v LogoVariant) []byte {
if v == LogoDark {
+2 -3
View File
@@ -46,8 +46,8 @@ func (a *Codex) DisplayName() string { return "Codex" }
func (a *Codex) Description() string {
return "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol."
}
func (a *Codex) Enabled() bool { return true }
func (a *Codex) DisplayOrder() int { return 2 }
func (a *Codex) Enabled() bool { return true }
func (a *Codex) DisplayOrder() int { return 2 }
func (a *Codex) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -79,7 +79,6 @@ func (a *Codex) BuildCommand(opts CommandOptions) Command {
Build()
}
func (a *Codex) Runtime() *RuntimeConfig {
canRecover := true
return &RuntimeConfig{
@@ -47,8 +47,8 @@ func (a *Copilot) DisplayName() string { return "Copilot" }
func (a *Copilot) Description() string {
return "GitHub Copilot CLI-powered autonomous coding agent using the Copilot SDK protocol."
}
func (a *Copilot) Enabled() bool { return true }
func (a *Copilot) DisplayOrder() int { return 6 }
func (a *Copilot) Enabled() bool { return true }
func (a *Copilot) DisplayOrder() int { return 6 }
func (a *Copilot) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -92,7 +92,6 @@ func (a *Copilot) BuildCommand(opts CommandOptions) Command {
Build()
}
func (a *Copilot) Runtime() *RuntimeConfig {
canRecover := true
return &RuntimeConfig{
+2 -2
View File
@@ -48,8 +48,8 @@ func (a *Gemini) DisplayName() string { return "Gemini" }
func (a *Gemini) Description() string {
return "Google Gemini CLI-powered autonomous coding agent using ACP protocol."
}
func (a *Gemini) Enabled() bool { return true }
func (a *Gemini) DisplayOrder() int { return 5 }
func (a *Gemini) Enabled() bool { return true }
func (a *Gemini) DisplayOrder() int { return 5 }
func (a *Gemini) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -126,4 +126,3 @@ func execAndParse(ctx context.Context, timeout time.Duration, parse func(string)
return parse(stdout.String())
}
+5 -5
View File
@@ -35,8 +35,8 @@ func (a *MockAgent) DisplayName() string { return "Mock" }
func (a *MockAgent) Description() string {
return "Mock agent for testing. Generates simulated responses with all message types."
}
func (a *MockAgent) Enabled() bool { return a.enabled }
func (a *MockAgent) DisplayOrder() int { return 99 }
func (a *MockAgent) Enabled() bool { return a.enabled }
func (a *MockAgent) DisplayOrder() int { return 99 }
func (a *MockAgent) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -69,9 +69,9 @@ func (a *MockAgent) BuildCommand(opts CommandOptions) Command {
func (a *MockAgent) Runtime() *RuntimeConfig {
canRecover := false
return &RuntimeConfig{
Cmd: Cmd("mock-agent").Build(),
WorkingDir: "{workspace}",
Env: map[string]string{},
Cmd: Cmd("mock-agent").Build(),
WorkingDir: "{workspace}",
Env: map[string]string{},
ResourceLimits: ResourceLimits{MemoryMB: 512, CPUCores: 0.5, Timeout: time.Hour},
Protocol: agent.ProtocolClaudeCode,
ModelFlag: NewParam("--model", "{model}"),
@@ -49,8 +49,8 @@ func (a *OpenCode) DisplayName() string { return "OpenCode" }
func (a *OpenCode) Description() string {
return "OpenCode CLI-powered autonomous coding agent using REST/SSE protocol."
}
func (a *OpenCode) Enabled() bool { return true }
func (a *OpenCode) DisplayOrder() int { return 4 }
func (a *OpenCode) Enabled() bool { return true }
func (a *OpenCode) DisplayOrder() int { return 4 }
func (a *OpenCode) Logo(v LogoVariant) []byte {
if v == LogoDark {
@@ -12,4 +12,3 @@ var (
// ErrAgentNotFound is returned when agent is not found
ErrAgentNotFound = errors.New("agent not found")
)
@@ -99,4 +99,3 @@ func (p *AugmentSessionProvider) ListAvailable(ctx context.Context) ([]string, e
return []string{}, nil
}
@@ -138,4 +138,3 @@ func (p *EnvProvider) ListAvailable(ctx context.Context) ([]string, error) {
return available, nil
}
@@ -111,4 +111,3 @@ func (p *FileProvider) Reload() error {
return p.load()
}
@@ -182,4 +182,3 @@ func (m *Manager) ClearCache() {
m.cache = make(map[string]*Credential)
m.logger.Debug("credential cache cleared")
}
+3 -4
View File
@@ -73,8 +73,8 @@ type ListAgentTypesRequest struct{}
// AgentTypeDTO represents an agent type
type AgentTypeDTO struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Image string `json:"image"`
Enabled bool `json:"enabled"`
@@ -124,7 +124,7 @@ type AgentExecutionData struct {
// FromAgentType converts a registry.AgentType to AgentTypeDTO
func FromAgentType(t *AgentTypeData) AgentTypeDTO {
return AgentTypeDTO{
ID: t.ID,
ID: t.ID,
Name: t.Name,
Description: t.Description,
Image: t.Image,
@@ -140,4 +140,3 @@ type AgentTypeData struct {
Image string
Enabled bool
}
@@ -413,4 +413,3 @@ func (h *GitHandlers) getAgentCtlClient(sessionID string) (*client.Client, error
return c, nil
}
@@ -159,4 +159,3 @@ func (h *Handlers) wsListAgentTypes(ctx context.Context, msg *ws.Message) (*ws.M
}
return ws.NewResponse(msg.ID, msg.Action, resp)
}
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/agent/lifecycle"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/common/logger"
ws "github.com/kandev/kandev/pkg/websocket"
"go.uber.org/zap"
@@ -13,8 +13,8 @@ import (
// WorkspaceFileHandlers handles workspace file operations
type WorkspaceFileHandlers struct {
lifecycle *lifecycle.Manager
logger *logger.Logger
lifecycle *lifecycle.Manager
logger *logger.Logger
}
// NewWorkspaceFileHandlers creates new workspace file handlers
@@ -11,9 +11,9 @@ import (
"go.uber.org/zap"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/docker"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/common/logger"
)
@@ -294,11 +294,11 @@ func (p ContextWindowEventPayload) GetSessionID() string {
// AvailableCommandsEventPayload is the payload for available commands update events.
type AvailableCommandsEventPayload struct {
TaskID string `json:"task_id"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id"`
TaskID string `json:"task_id"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id"`
AvailableCommands []streams.AvailableCommand `json:"available_commands"`
Timestamp string `json:"timestamp"`
Timestamp string `json:"timestamp"`
}
// GetSessionID returns the session ID for this event (used by event routing).
@@ -144,8 +144,6 @@ func (s *ExecutionStore) UpdateError(executionID string, errorMsg string) {
}
}
// WithLock executes a function with the store lock held, providing access to the execution.
// Returns ErrExecutionNotFound if the execution doesn't exist.
// The function should be fast to avoid blocking other operations.
@@ -349,4 +349,3 @@ func (m *Manager) handleProcessStatus(execution *AgentExecution, status *agentct
func (m *Manager) handleShellExit(execution *AgentExecution, code int) {
m.eventPublisher.PublishShellExit(execution, code)
}
@@ -388,4 +388,3 @@ func (m *Manager) initializeAgentSession(ctx context.Context, execution *AgentEx
m.finalizeBootMessage(bootMsg, bootStopCh, execution.agentctl, containerStateExited)
return nil
}
@@ -290,7 +290,6 @@ func TestRuntimeRegistry_RecoverAll(t *testing.T) {
})
}
func TestRuntimeRegistry_ThreadSafety(t *testing.T) {
log := newTestRegistryLogger()
registry := NewRuntimeRegistry(log)
@@ -349,4 +348,3 @@ func TestRuntimeRegistry_ThreadSafety(t *testing.T) {
// If we reach here without a race condition panic, the test passes
// The -race flag during testing will detect actual race conditions
}
@@ -15,8 +15,8 @@ import (
"github.com/kandev/kandev/internal/common/constants"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/sysprompt"
v1 "github.com/kandev/kandev/pkg/api/v1"
"github.com/kandev/kandev/pkg/agent"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
// SessionManager handles ACP session initialization and management
@@ -36,15 +36,15 @@ type SessionHistoryManager struct {
// HistoryEntry represents a single entry in the session history.
type HistoryEntry struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"` // "user_message", "agent_message", "tool_call", "tool_result"
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolStatus string `json:"tool_status,omitempty"`
SessionID string `json:"session_id,omitempty"`
OperationID string `json:"operation_id,omitempty"`
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"` // "user_message", "agent_message", "tool_call", "tool_result"
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolStatus string `json:"tool_status,omitempty"`
SessionID string `json:"session_id,omitempty"`
OperationID string `json:"operation_id,omitempty"`
}
// NewSessionHistoryManager creates a new SessionHistoryManager.
@@ -30,8 +30,8 @@ func newSessionTestLogger() *logger.Logger {
// mockAgentServer creates a test WebSocket server simulating agentctl.
// It responds to agent stream requests and tracks which actions were called and in what order.
type mockAgentServer struct {
server *httptest.Server
mu sync.Mutex
server *httptest.Server
mu sync.Mutex
actionLog []string // ordered log of actions received
upgrader websocket.Upgrader
handler func(msg ws.Message) *ws.Message
@@ -19,8 +19,8 @@ type testAgent struct {
runtime *agents.RuntimeConfig
}
func (a *testAgent) ID() string { return a.id }
func (a *testAgent) Name() string { return a.name }
func (a *testAgent) ID() string { return a.id }
func (a *testAgent) Name() string { return a.name }
func (a *testAgent) DisplayName() string { return a.name }
func (a *testAgent) Description() string { return a.description }
func (a *testAgent) Enabled() bool { return a.enabled }
@@ -58,9 +58,9 @@ func validAgentConfig(id, name string) *testAgent {
Tag: "latest",
WorkingDir: "/workspace",
ResourceLimits: agents.ResourceLimits{
MemoryMB: 1024,
CPUCores: 1.0,
Timeout: time.Hour,
MemoryMB: 1024,
CPUCores: 1.0,
Timeout: time.Hour,
},
},
}
@@ -35,7 +35,6 @@ func (a *testAgent) DisplayOrder() int { return 0 }
func (a *testAgent) Logo(v agents.LogoVariant) []byte { return a.logoData }
func (a *testAgent) IsInstalled(ctx context.Context) (*agents.DiscoveryResult, error) {
return &agents.DiscoveryResult{Available: false}, nil
}
@@ -342,6 +342,7 @@ func (c *Client) Cancel(ctx context.Context) error {
}
return nil
}
// GetAgentStderr returns recent stderr lines from the agent process via the agent WebSocket stream.
func (c *Client) GetAgentStderr(ctx context.Context) ([]string, error) {
resp, err := c.sendStreamRequest(ctx, "agent.stderr", nil)
@@ -231,8 +231,6 @@ func (l *Launcher) Stop(ctx context.Context) error {
}
}
// checkPortAvailable verifies the given port is not in use.
// It checks by attempting a wildcard bind (matching what agentctl does with ":port").
func checkPortAvailable(port int) error {
@@ -351,4 +351,3 @@ func (c *Client) WaitForTerminalExit(ctx context.Context, p acp.WaitForTerminalE
// Verify interface implementation
var _ acp.Client = (*Client)(nil)
@@ -13,8 +13,8 @@ import (
"github.com/kandev/kandev/internal/agentctl/types"
"github.com/kandev/kandev/internal/agentctl/types/streams"
"github.com/kandev/kandev/internal/common/logger"
v1 "github.com/kandev/kandev/pkg/api/v1"
"github.com/kandev/kandev/pkg/amp"
v1 "github.com/kandev/kandev/pkg/api/v1"
"go.uber.org/zap"
)
@@ -373,8 +373,8 @@ func (a *CopilotAdapter) Prompt(ctx context.Context, message string, _ []v1.Mess
operationID := uuid.New().String()
a.operationID = operationID
a.resultCh = make(chan resultComplete, 1)
a.completeSent = false // Reset completion flag for new operation
a.receivedDeltas = false // Reset delta tracking for new operation
a.completeSent = false // Reset completion flag for new operation
a.receivedDeltas = false // Reset delta tracking for new operation
a.mu.Unlock()
a.logger.Info("sending prompt via SDK",
@@ -982,7 +982,7 @@ func (a *CopilotAdapter) resolvePermissionActionDetails(
if se := cachedPayload.ShellExec(); se != nil {
return se.Command, types.ActionTypeCommand, map[string]interface{}{
"command": se.Command,
"cwd": se.WorkDir,
"cwd": se.WorkDir,
}
}
case streams.ToolKindModifyFile:
@@ -167,4 +167,3 @@ func (n *Normalizer) normalizeCommandResult(payload *streams.ShellExecPayload, r
}
}
}
@@ -159,4 +159,3 @@ func (a *Adapter) handleHookCallback(requestID string, req *claudecode.ControlRe
a.logger.Warn("failed to send hook callback response", zap.Error(err))
}
}
@@ -142,4 +142,3 @@ func (i *Instance) Info() *InstanceInfo {
CreatedAt: i.CreatedAt,
}
}
@@ -123,4 +123,3 @@ func (c *ChannelBackendClient) RequestPayload(ctx context.Context, action string
func (c *ChannelBackendClient) Close() {
close(c.requestCh)
}
@@ -364,4 +364,3 @@ func (s *Server) deleteTaskPlanHandler() server.ToolHandlerFunc {
return mcp.NewToolResultText("Plan deleted successfully."), nil
}
}
@@ -14,11 +14,11 @@ func TestEscapeArg(t *testing.T) {
{"with tab", "hello\tworld", `"hello` + "\t" + `world"`},
{"with quote", `say "hi"`, `"say \"hi\""`},
{"backslash no quote", `a\b`, `a\b`},
{"backslash before quote", `a\"`, `a\\\"` },
{"backslash before quote", `a\"`, `a\\\"`},
{"trailing backslash with space", `a b\`, `"a b\\"`},
{"multiple trailing backslashes with space", `a b\\`, `"a b\\\\"`},
{"only backslashes", `\\`, `\\`},
{"backslash then quote", `\\"`, `\\\\\"` },
{"backslash then quote", `\\"`, `\\\\\"`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -36,12 +36,12 @@ type interactiveProcess struct {
// Deferred start - process created lazily on first resize
// This ensures PTY is created at exact frontend dimensions
started bool
startOnce sync.Once
startCmd []string
startDir string
startEnv map[string]string
startReq InteractiveStartRequest // Full request for deferred initialization
started bool
startOnce sync.Once
startCmd []string
startDir string
startEnv map[string]string
startReq InteractiveStartRequest // Full request for deferred initialization
// Direct output - when set, raw output goes here instead of event bus
directOutput DirectOutputWriter
@@ -18,22 +18,22 @@ import (
// InteractiveStartRequest contains parameters for starting an interactive passthrough process.
type InteractiveStartRequest struct {
SessionID string `json:"session_id"` // Required: Agent session owning this process
Command []string `json:"command"` // Required: Command and args to execute
WorkingDir string `json:"working_dir"` // Working directory
Env map[string]string `json:"env,omitempty"` // Additional environment variables
PromptPattern string `json:"prompt_pattern,omitempty"` // Regex pattern to detect agent prompt for turn completion
IdleTimeout time.Duration `json:"idle_timeout,omitempty"` // Idle timeout for turn detection
BufferMaxBytes int64 `json:"buffer_max_bytes,omitempty"` // Max output buffer size
StatusDetector string `json:"status_detector,omitempty"` // Status detector type: "claude_code", "codex", ""
CheckInterval time.Duration `json:"check_interval,omitempty"` // How often to check state (default 100ms)
StabilityWindow time.Duration `json:"stability_window,omitempty"` // State stability window (default 0)
ImmediateStart bool `json:"immediate_start,omitempty"` // Start immediately with default dimensions (don't wait for resize)
DefaultCols int `json:"default_cols,omitempty"` // Default columns if ImmediateStart (default 120)
DefaultRows int `json:"default_rows,omitempty"` // Default rows if ImmediateStart (default 40)
InitialCommand string `json:"initial_command,omitempty"` // Command to write to stdin after shell starts (for script terminals)
DisableTurnDetection bool `json:"disable_turn_detection,omitempty"` // Disable idle timer and turn detection (for user shell terminals)
IsUserShell bool `json:"is_user_shell,omitempty"` // Mark as user shell process (excluded from session-level lookups)
SessionID string `json:"session_id"` // Required: Agent session owning this process
Command []string `json:"command"` // Required: Command and args to execute
WorkingDir string `json:"working_dir"` // Working directory
Env map[string]string `json:"env,omitempty"` // Additional environment variables
PromptPattern string `json:"prompt_pattern,omitempty"` // Regex pattern to detect agent prompt for turn completion
IdleTimeout time.Duration `json:"idle_timeout,omitempty"` // Idle timeout for turn detection
BufferMaxBytes int64 `json:"buffer_max_bytes,omitempty"` // Max output buffer size
StatusDetector string `json:"status_detector,omitempty"` // Status detector type: "claude_code", "codex", ""
CheckInterval time.Duration `json:"check_interval,omitempty"` // How often to check state (default 100ms)
StabilityWindow time.Duration `json:"stability_window,omitempty"` // State stability window (default 0)
ImmediateStart bool `json:"immediate_start,omitempty"` // Start immediately with default dimensions (don't wait for resize)
DefaultCols int `json:"default_cols,omitempty"` // Default columns if ImmediateStart (default 120)
DefaultRows int `json:"default_rows,omitempty"` // Default rows if ImmediateStart (default 40)
InitialCommand string `json:"initial_command,omitempty"` // Command to write to stdin after shell starts (for script terminals)
DisableTurnDetection bool `json:"disable_turn_detection,omitempty"` // Disable idle timer and turn detection (for user shell terminals)
IsUserShell bool `json:"is_user_shell,omitempty"` // Mark as user shell process (excluded from session-level lookups)
}
// InteractiveProcessInfo represents the state of an interactive process.
@@ -90,9 +90,9 @@ type userShellEntry struct {
// InteractiveRunner manages interactive PTY-based processes with stdin support.
type InteractiveRunner struct {
logger *logger.Logger
workspaceTracker *WorkspaceTracker
bufferMaxBytes int64
logger *logger.Logger
workspaceTracker *WorkspaceTracker
bufferMaxBytes int64
turnCompleteCallback TurnCompleteCallback
outputCallback OutputCallback
statusCallback StatusCallback
@@ -30,15 +30,15 @@ func (s *stubAdapter) Connect(stdin io.Writer, stdout io.Reader) error {
s.connectCalled = true
return nil
}
func (s *stubAdapter) Initialize(context.Context) error { return nil }
func (s *stubAdapter) GetAgentInfo() *adapter.AgentInfo { return nil }
func (s *stubAdapter) Initialize(context.Context) error { return nil }
func (s *stubAdapter) GetAgentInfo() *adapter.AgentInfo { return nil }
func (s *stubAdapter) NewSession(_ context.Context, _ []types.McpServer) (string, error) {
return "", nil
}
func (s *stubAdapter) LoadSession(context.Context, string) error { return nil }
func (s *stubAdapter) Prompt(context.Context, string, []v1.MessageAttachment) error { return nil }
func (s *stubAdapter) Cancel(context.Context) error { return nil }
func (s *stubAdapter) Updates() <-chan adapter.AgentEvent { return s.updatesCh }
func (s *stubAdapter) Updates() <-chan adapter.AgentEvent { return s.updatesCh }
func (s *stubAdapter) GetSessionID() string { return "" }
func (s *stubAdapter) GetOperationID() string { return "" }
func (s *stubAdapter) SetPermissionHandler(adapter.PermissionHandler) {}
@@ -24,4 +24,3 @@ func killProcessGroup(pid int) error {
// Kill the entire process group by using negative PID
return syscall.Kill(-pid, syscall.SIGKILL)
}
@@ -21,4 +21,3 @@ func killProcessGroup(pid int) error {
// Kill the entire process group by using negative PID
return syscall.Kill(-pid, syscall.SIGKILL)
}
@@ -26,4 +26,3 @@ func killProcessGroup(pid int) error {
kill := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
return kill.Run()
}
@@ -61,4 +61,3 @@ func startPTYWithSize(cmd *exec.Cmd, cols, rows int) (PtyHandle, error) {
return &windowsPTY{cpty: cpty}, nil
}
@@ -12,10 +12,10 @@
// - Per-session process tracking
//
// Lifecycle:
// 1. Start() - Spawns process, creates output pipes, returns immediately
// 2. Background goroutines - Stream stdout/stderr, wait for exit
// 3. Stop() - Sends SIGTERM to process group, escalates to SIGKILL after 2s
// 4. Automatic cleanup - Process removed from tracking after exit
// 1. Start() - Spawns process, creates output pipes, returns immediately
// 2. Background goroutines - Stream stdout/stderr, wait for exit
// 3. Stop() - Sends SIGTERM to process group, escalates to SIGKILL after 2s
// 4. Automatic cleanup - Process removed from tracking after exit
//
// Use Cases:
// - Agent-triggered builds (npm run build, go test ./...)
@@ -43,12 +43,12 @@ import (
// StartProcessRequest contains parameters for starting a new background process.
type StartProcessRequest struct {
SessionID string `json:"session_id"` // Required: Agent session owning this process
Kind types.ProcessKind `json:"kind"` // Process type (user_command, agent_script, etc.)
ScriptName string `json:"script_name,omitempty"` // Human-readable script identifier
Command string `json:"command"` // Required: Shell command to execute (passed to sh -lc)
WorkingDir string `json:"working_dir"` // Working directory (defaults to current dir)
Env map[string]string `json:"env,omitempty"` // Additional environment variables (merged with parent env)
SessionID string `json:"session_id"` // Required: Agent session owning this process
Kind types.ProcessKind `json:"kind"` // Process type (user_command, agent_script, etc.)
ScriptName string `json:"script_name,omitempty"` // Human-readable script identifier
Command string `json:"command"` // Required: Shell command to execute (passed to sh -lc)
WorkingDir string `json:"working_dir"` // Working directory (defaults to current dir)
Env map[string]string `json:"env,omitempty"` // Additional environment variables (merged with parent env)
BufferMaxBytes int64 `json:"buffer_max_bytes,omitempty"` // Max output buffer size (defaults to runner's default)
}
@@ -59,17 +59,17 @@ type StopProcessRequest struct {
// ProcessInfo represents the complete state of a background process.
type ProcessInfo struct {
ID string `json:"id"` // Unique process identifier (UUID)
SessionID string `json:"session_id"` // Agent session that owns this process
Kind types.ProcessKind `json:"kind"` // Process classification
ID string `json:"id"` // Unique process identifier (UUID)
SessionID string `json:"session_id"` // Agent session that owns this process
Kind types.ProcessKind `json:"kind"` // Process classification
ScriptName string `json:"script_name,omitempty"` // User-friendly name
Command string `json:"command"` // The shell command being executed
WorkingDir string `json:"working_dir"` // Execution directory
Status types.ProcessStatus `json:"status"` // Current state (starting, running, exited, failed)
ExitCode *int `json:"exit_code,omitempty"` // Process exit code (nil if still running)
StartedAt time.Time `json:"started_at"` // When the process was launched
UpdatedAt time.Time `json:"updated_at"` // Last status change timestamp
Output []ProcessOutputChunk `json:"output,omitempty"` // Buffered output (only included if requested)
Command string `json:"command"` // The shell command being executed
WorkingDir string `json:"working_dir"` // Execution directory
Status types.ProcessStatus `json:"status"` // Current state (starting, running, exited, failed)
ExitCode *int `json:"exit_code,omitempty"` // Process exit code (nil if still running)
StartedAt time.Time `json:"started_at"` // When the process was launched
UpdatedAt time.Time `json:"updated_at"` // Last status change timestamp
Output []ProcessOutputChunk `json:"output,omitempty"` // Buffered output (only included if requested)
}
// ProcessOutputChunk represents a single piece of process output (stdout or stderr).
@@ -134,12 +134,12 @@ func (b *ringBuffer) snapshot() []ProcessOutputChunk {
// commandProcess represents a single running background process and its state.
type commandProcess struct {
info ProcessInfo // Process metadata and current status
cmd *exec.Cmd // Underlying OS process (nil after completion)
buffer *ringBuffer // Memory-bounded output storage
stopOnce sync.Once // Ensures stopSignal is only closed once
stopSignal chan struct{} // Signals output readers to exit before process termination
mu sync.Mutex // Protects info fields during updates
info ProcessInfo // Process metadata and current status
cmd *exec.Cmd // Underlying OS process (nil after completion)
buffer *ringBuffer // Memory-bounded output storage
stopOnce sync.Once // Ensures stopSignal is only closed once
stopSignal chan struct{} // Signals output readers to exit before process termination
mu sync.Mutex // Protects info fields during updates
}
// ProcessRunner manages multiple background processes with output streaming.
@@ -147,11 +147,11 @@ type commandProcess struct {
// Thread-safe: All public methods can be called concurrently from multiple goroutines.
//
// Process Lifecycle:
// 1. Start() creates a process in "starting" state
// 2. Transitions to "running" after successful spawn
// 3. Background goroutines stream output and wait for exit
// 4. Final status is "exited" (exit code 0) or "failed" (non-zero exit code)
// 5. Process is automatically removed from tracking after completion
// 1. Start() creates a process in "starting" state
// 2. Transitions to "running" after successful spawn
// 3. Background goroutines stream output and wait for exit
// 4. Final status is "exited" (exit code 0) or "failed" (non-zero exit code)
// 5. Process is automatically removed from tracking after completion
//
// Output Handling:
// - Stdout and stderr are captured separately and streamed to WebSocket
@@ -167,18 +167,18 @@ type ProcessRunner struct {
workspaceTracker *WorkspaceTracker // WebSocket stream coordinator (can be nil)
bufferMaxBytes int64 // Default output buffer size for new processes
mu sync.RWMutex // Protects processes map
processes map[string]*commandProcess // Active processes by ID
mu sync.RWMutex // Protects processes map
processes map[string]*commandProcess // Active processes by ID
}
// NewProcessRunner creates a new process runner.
//
// Parameters:
// - workspaceTracker: Optional coordinator for streaming output to WebSocket clients.
// If nil, processes run but output isn't streamed (only buffered).
// If nil, processes run but output isn't streamed (only buffered).
// - log: Logger instance for process lifecycle events
// - bufferMaxBytes: Default output buffer size (typically 2MB). Individual processes
// can override via StartProcessRequest.BufferMaxBytes.
// can override via StartProcessRequest.BufferMaxBytes.
func NewProcessRunner(workspaceTracker *WorkspaceTracker, log *logger.Logger, bufferMaxBytes int64) *ProcessRunner {
return &ProcessRunner{
logger: log.WithFields(zap.String("component", "process-runner")),
@@ -409,7 +409,7 @@ func (r *ProcessRunner) StopAll(ctx context.Context) error {
// Parameters:
// - id: Process UUID returned by Start()
// - includeOutput: If true, includes buffered stdout/stderr in the response.
// Set to false for status checks to avoid copying large buffers.
// Set to false for status checks to avoid copying large buffers.
//
// Returns (ProcessInfo, true) if found, or (nil, false) if not found.
//
@@ -428,7 +428,7 @@ func (r *ProcessRunner) Get(id string, includeOutput bool) (*ProcessInfo, bool)
//
// Parameters:
// - sessionID: If non-empty, returns only processes for this session.
// If empty, returns all processes across all sessions.
// If empty, returns all processes across all sessions.
//
// Output is always returned without buffered output (includeOutput=false).
// Use Get(id, true) to retrieve output for a specific process.
@@ -486,11 +486,11 @@ func (r *ProcessRunner) readOutput(proc *commandProcess, reader io.ReadCloser, s
// wait blocks until the process exits, then updates final status and cleans up.
//
// This runs in a background goroutine spawned by Start(). Responsibilities:
// 1. Wait for process termination (blocks on cmd.Wait())
// 2. Extract exit code from process state
// 3. Determine final status: "exited" (code 0) vs "failed" (non-zero)
// 4. Publish final status update to WebSocket clients
// 5. Remove process from tracking map (makes ID unavailable for future lookups)
// 1. Wait for process termination (blocks on cmd.Wait())
// 2. Extract exit code from process state
// 3. Determine final status: "exited" (code 0) vs "failed" (non-zero)
// 4. Publish final status update to WebSocket clients
// 5. Remove process from tracking map (makes ID unavailable for future lookups)
//
// Exit Code Extraction:
// - Success (exit code 0): Status = "exited"
@@ -596,7 +596,7 @@ func (r *ProcessRunner) publishStatus(proc *commandProcess) {
//
// Parameters:
// - includeOutput: If true, includes buffered output chunks (can be large).
// If false, returns info without output (faster, smaller).
// If false, returns info without output (faster, smaller).
//
// Thread-safe: Locks process mutex to prevent races with status updates.
func (p *commandProcess) snapshot(includeOutput bool) ProcessInfo {
@@ -612,14 +612,15 @@ func (p *commandProcess) snapshot(includeOutput bool) ProcessInfo {
// mergeEnv merges custom environment variables with the parent process environment.
//
// Strategy:
// 1. Start with all environment variables from os.Environ() (parent process)
// 2. Override/add variables from the env map (custom variables)
// 3. Return as []string in "KEY=VALUE" format (required by exec.Cmd.Env)
// 1. Start with all environment variables from os.Environ() (parent process)
// 2. Override/add variables from the env map (custom variables)
// 3. Return as []string in "KEY=VALUE" format (required by exec.Cmd.Env)
//
// Example:
// Parent env: PATH=/usr/bin, HOME=/home/user
// Custom env: PATH=/custom/bin, FOO=bar
// Result: PATH=/custom/bin, HOME=/home/user, FOO=bar
//
// Parent env: PATH=/usr/bin, HOME=/home/user
// Custom env: PATH=/custom/bin, FOO=bar
// Result: PATH=/custom/bin, HOME=/home/user, FOO=bar
//
// This allows processes to inherit the agentctl server's environment (including PATH,
// HOME, etc.) while also supporting custom variables per process.
@@ -40,10 +40,10 @@ type StateChangeCallback func(sessionID string, state AgentState)
// StatusTrackerConfig contains configuration for the status tracker.
type StatusTrackerConfig struct {
Rows int // Terminal rows (default 24)
Cols int // Terminal columns (default 80)
CheckInterval time.Duration // How often to check state (default 100ms)
StabilityWindow time.Duration // Time state must be stable before reporting (default 0)
Rows int // Terminal rows (default 24)
Cols int // Terminal columns (default 80)
CheckInterval time.Duration // How often to check state (default 100ms)
StabilityWindow time.Duration // Time state must be stable before reporting (default 0)
}
// DefaultStatusTrackerConfig returns the default configuration.
@@ -58,11 +58,11 @@ func DefaultStatusTrackerConfig() StatusTrackerConfig {
// StatusTracker uses a virtual terminal emulator to track agent state.
type StatusTracker struct {
logger *logger.Logger
sessionID string
detector StatusDetector
callback StateChangeCallback
config StatusTrackerConfig
logger *logger.Logger
sessionID string
detector StatusDetector
callback StateChangeCallback
config StatusTrackerConfig
// vt10x terminal emulator
term vt10x.Terminal
@@ -72,8 +72,8 @@ type StatusTracker struct {
lastState AgentState
lastStateChange time.Time
lastCheck time.Time
pendingState AgentState // State waiting for stability window
pendingStateTime time.Time // When pending state was first detected
pendingState AgentState // State waiting for stability window
pendingStateTime time.Time // When pending state was first detected
}
// NewStatusTracker creates a new status tracker with the given detector.
@@ -98,13 +98,13 @@ func NewStatusTracker(
term := vt10x.New(vt10x.WithSize(config.Cols, config.Rows))
return &StatusTracker{
logger: log.WithFields(zap.String("component", "status-tracker"), zap.String("session_id", sessionID)),
sessionID: sessionID,
detector: detector,
callback: callback,
config: config,
term: term,
lastState: StateUnknown,
logger: log.WithFields(zap.String("component", "status-tracker"), zap.String("session_id", sessionID)),
sessionID: sessionID,
detector: detector,
callback: callback,
config: config,
term: term,
lastState: StateUnknown,
}
}
@@ -10,7 +10,7 @@ import (
// mockDetector is a test detector that returns configurable states
type mockDetector struct {
state AgentState
state AgentState
acceptTransition bool
}
@@ -550,4 +550,3 @@ func TestDetectShellFallback(t *testing.T) {
t.Logf("Detected shell: %q (may be valid if it exists on this system)", shell)
}
}
@@ -52,4 +52,3 @@
//
// All streams use JSON-encoded messages over WebSocket connections.
package streams
@@ -100,4 +100,3 @@ type PermissionRespondResponse struct {
// Error contains error message if Success is false.
Error string `json:"error,omitempty"`
}
@@ -61,4 +61,3 @@ type ShellBufferResponse struct {
// Data contains the buffered shell output.
Data string `json:"data"`
}
@@ -58,18 +58,18 @@ type NormalizedPayload struct {
// --- Getters for NormalizedPayload ---
func (p *NormalizedPayload) Kind() ToolKind { return p.kind }
func (p *NormalizedPayload) ReadFile() *ReadFilePayload { return p.readFile }
func (p *NormalizedPayload) ModifyFile() *ModifyFilePayload { return p.modifyFile }
func (p *NormalizedPayload) ShellExec() *ShellExecPayload { return p.shellExec }
func (p *NormalizedPayload) CodeSearch() *CodeSearchPayload { return p.codeSearch }
func (p *NormalizedPayload) HttpRequest() *HttpRequestPayload { return p.httpRequest }
func (p *NormalizedPayload) Generic() *GenericPayload { return p.generic }
func (p *NormalizedPayload) CreateTask() *CreateTaskPayload { return p.createTask }
func (p *NormalizedPayload) Kind() ToolKind { return p.kind }
func (p *NormalizedPayload) ReadFile() *ReadFilePayload { return p.readFile }
func (p *NormalizedPayload) ModifyFile() *ModifyFilePayload { return p.modifyFile }
func (p *NormalizedPayload) ShellExec() *ShellExecPayload { return p.shellExec }
func (p *NormalizedPayload) CodeSearch() *CodeSearchPayload { return p.codeSearch }
func (p *NormalizedPayload) HttpRequest() *HttpRequestPayload { return p.httpRequest }
func (p *NormalizedPayload) Generic() *GenericPayload { return p.generic }
func (p *NormalizedPayload) CreateTask() *CreateTaskPayload { return p.createTask }
func (p *NormalizedPayload) SubagentTask() *SubagentTaskPayload { return p.subagentTask }
func (p *NormalizedPayload) ShowPlan() *ShowPlanPayload { return p.showPlan }
func (p *NormalizedPayload) ManageTodos() *ManageTodosPayload { return p.manageTodos }
func (p *NormalizedPayload) Misc() *MiscPayload { return p.misc }
func (p *NormalizedPayload) ShowPlan() *ShowPlanPayload { return p.showPlan }
func (p *NormalizedPayload) ManageTodos() *ManageTodosPayload { return p.manageTodos }
func (p *NormalizedPayload) Misc() *MiscPayload { return p.misc }
// MarshalJSON implements custom JSON marshaling for NormalizedPayload.
func (p *NormalizedPayload) MarshalJSON() ([]byte, error) {
@@ -202,8 +202,8 @@ func (h *Handlers) httpRespond(c *gin.Context) {
zap.String("pending_id", pendingID),
zap.Error(err))
c.JSON(http.StatusGone, gin.H{
"error": "clarification request expired",
"code": "CLARIFICATION_EXPIRED",
"error": "clarification request expired",
"code": "CLARIFICATION_EXPIRED",
"message": "This question has timed out. The agent may have already moved on.",
})
return
@@ -261,4 +261,3 @@ func (h *Handlers) markRequestExpired(c *gin.Context, pendingID string) {
func generateOptionID(questionIndex, optionIndex int) string {
return fmt.Sprintf("q%d_opt%d", questionIndex+1, optionIndex+1)
}
+6 -7
View File
@@ -13,9 +13,9 @@ import (
// Store manages pending clarification requests.
// It provides thread-safe storage and notification when responses arrive.
type Store struct {
mu sync.RWMutex
pending map[string]*PendingClarification
timeout time.Duration
mu sync.RWMutex
pending map[string]*PendingClarification
timeout time.Duration
}
// NewStore creates a new clarification store.
@@ -131,10 +131,10 @@ func (s *Store) Cancel(pendingID string) error {
// Send a cancelled response
select {
case pending.ResponseCh <- &Response{
PendingID: pendingID,
Rejected: true,
PendingID: pendingID,
Rejected: true,
RejectReason: "cancelled",
RespondedAt: time.Now(),
RespondedAt: time.Now(),
}:
default:
}
@@ -181,4 +181,3 @@ func (s *Store) ListPending() []*Request {
}
return requests
}
@@ -64,4 +64,3 @@ const (
StatusExpired Status = "expired"
StatusCancelled Status = "cancelled"
)
@@ -213,4 +213,3 @@ func (l *Logger) Zap() *zap.Logger {
func (l *Logger) Sugar() *zap.SugaredLogger {
return l.sugar
}
@@ -35,14 +35,15 @@ func AllocatePort() (int, error) {
// Multiple occurrences of the same placeholder get the same port.
//
// Examples:
// Input: "npm run dev -- --port $PORT"
// Output: "npm run dev -- --port 54321", {"PORT": "54321"}
//
// Input: "vite --port ${PORT}"
// Output: "vite --port 54321", {"PORT": "54321"}
// Input: "npm run dev -- --port $PORT"
// Output: "npm run dev -- --port 54321", {"PORT": "54321"}
//
// Input: "npm run dev" (no placeholder)
// Output: "npm run dev", {}
// Input: "vite --port ${PORT}"
// Output: "vite --port 54321", {"PORT": "54321"}
//
// Input: "npm run dev" (no placeholder)
// Output: "npm run dev", {}
func TransformCommand(command string) (string, map[string]string, error) {
// Find all unique placeholder names
uniquePlaceholders := findUniquePlaceholders(command)
+2 -2
View File
@@ -16,8 +16,8 @@ import (
func getAdapterBaseDir() (string, error) {
// Try multiple possible paths
candidates := []string{
"apps/backend/internal/agentctl/server/adapter", // From git root
"internal/agentctl/server/adapter", // From apps/backend
"apps/backend/internal/agentctl/server/adapter", // From git root
"internal/agentctl/server/adapter", // From apps/backend
}
cwd, err := os.Getwd()
@@ -30,9 +30,9 @@ var (
// Editor kind constants for custom editor types.
const (
editorKindCustomRemoteSSH = "custom_remote_ssh"
editorKindCustomHostedURL = "custom_hosted_url"
editorKindCustomCommand = "custom_command"
editorKindCustomRemoteSSH = "custom_remote_ssh"
editorKindCustomHostedURL = "custom_hosted_url"
editorKindCustomCommand = "custom_command"
)
type UserSettingsProvider interface {
-1
View File
@@ -58,4 +58,3 @@ type EventBus interface {
// IsConnected returns connection status
IsConnected() bool
}
+7 -7
View File
@@ -24,13 +24,13 @@ type MemoryEventBus struct {
// memorySubscription represents an in-memory subscription
type memorySubscription struct {
bus *MemoryEventBus
subject string
pattern *regexp.Regexp // For wildcard matching
handler EventHandler
queue string // Empty for regular subscriptions
active bool
mu sync.Mutex
bus *MemoryEventBus
subject string
pattern *regexp.Regexp // For wildcard matching
handler EventHandler
queue string // Empty for regular subscriptions
active bool
mu sync.Mutex
}
// queueGroup manages load balancing for queue subscriptions
-1
View File
@@ -189,4 +189,3 @@ func (b *NATSEventBus) IsConnected() bool {
}
return b.conn.IsConnected()
}
@@ -22,4 +22,3 @@ func (s *natsSubscription) IsValid() bool {
}
return s.sub.IsValid()
}
-1
View File
@@ -248,4 +248,3 @@ func BuildAvailableCommandsSubject(sessionID string) string {
func BuildAvailableCommandsWildcardSubject() string {
return AvailableCommandsUpdated + ".*"
}
@@ -72,8 +72,6 @@ func (h *Handler) HandleConnection(c *gin.Context) {
client.ReadPump(c.Request.Context())
}
// RegisterHealthHandler registers the health check handler
func RegisterHealthHandler(d *ws.Dispatcher) {
d.RegisterFunc(ws.ActionHealthCheck, func(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
@@ -84,4 +82,3 @@ func RegisterHealthHandler(d *ws.Dispatcher) {
})
})
}
@@ -25,8 +25,8 @@ type SessionStreamBroadcaster struct {
// File change debouncing
fileChangeMu sync.Mutex
fileChangeBatch map[string][]any // sessionID -> list of file change payloads
fileChangeTimers map[string]*time.Timer // sessionID -> debounce timer
fileChangeBatch map[string][]any // sessionID -> list of file change payloads
fileChangeTimers map[string]*time.Timer // sessionID -> debounce timer
}
func RegisterSessionStreamNotifications(ctx context.Context, eventBus bus.EventBus, hub *Hub, log *logger.Logger) *SessionStreamBroadcaster {
@@ -62,4 +62,3 @@ func (g *Gateway) SetupRoutes(router *gin.Engine) {
router.GET("/lsp/:sessionId", g.LSPHandler.HandleLSPConnection)
}
}
@@ -11,13 +11,13 @@ import (
"time"
)
// OS name constants used for runtime.GOOS comparisons.
const (
osDarwin = "darwin"
osLinux = "linux"
osWindows = "windows"
)
type SystemProvider struct {
assets systemAssets
}
@@ -281,10 +281,10 @@ func (s *Service) ensureSystemProvider(ctx context.Context, userID string) error
return nil
}
provider := &models.Provider{
ID: uuid.New().String(),
UserID: userID,
Name: "System Notifications",
Type: models.ProviderTypeSystem,
ID: uuid.New().String(),
UserID: userID,
Name: "System Notifications",
Type: models.ProviderTypeSystem,
Config: map[string]interface{}{
"sound_enabled": false,
},
@@ -299,10 +299,10 @@ func (s *Service) handleToolUpdateEvent(ctx context.Context, payload *lifecycle.
payload.Data.ToolStatus,
"", // result - no longer used, tool results in NormalizedPayload
payload.SessionID,
payload.Data.ToolTitle, // Include title from update event
payload.Data.ToolTitle, // Include title from update event
s.getActiveTurnID(payload.SessionID), // Turn ID for fallback creation
msgType, // Message type for fallback creation
payload.Data.Normalized, // Pass normalized tool data for message metadata
msgType, // Message type for fallback creation
payload.Data.Normalized, // Pass normalized tool data for message metadata
); err != nil {
s.logger.Warn("failed to update tool call message",
zap.String("task_id", payload.TaskID),
@@ -9,8 +9,8 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/orchestrator/executor"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/repository"
@@ -738,4 +738,3 @@ func TestProcessOnEnterPassthrough(t *testing.T) {
}
})
}
@@ -93,8 +93,8 @@ type LaunchAgentRequest struct {
Priority int
Metadata map[string]interface{}
Env map[string]string
ACPSessionID string // ACP session ID to resume, if available
ModelOverride string // If set, use this model instead of the profile's model
ACPSessionID string // ACP session ID to resume, if available
ModelOverride string // If set, use this model instead of the profile's model
ExecutorType string // Executor type (e.g., "local", "worktree", "local_docker") - determines runtime
ExecutorConfig map[string]string // Executor config (docker_host, git_token, etc.)
@@ -209,4 +209,3 @@ func NewExecutor(agentManager AgentManagerClient, repo repository.Repository, lo
func (e *Executor) SetOnTaskStateChange(fn TaskStateChangeFunc) {
e.onTaskStateChange = fn
}
@@ -13,10 +13,10 @@ import (
// mockAgentManager implements AgentManagerClient for testing
type mockAgentManager struct {
launchAgentFunc func(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error)
startAgentProcessFunc func(ctx context.Context, agentExecutionID string) error
resolveAgentProfileFunc func(ctx context.Context, profileID string) (*AgentProfileInfo, error)
setExecutionDescriptionFunc func(ctx context.Context, agentExecutionID string, description string) error
launchAgentFunc func(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error)
startAgentProcessFunc func(ctx context.Context, agentExecutionID string) error
resolveAgentProfileFunc func(ctx context.Context, profileID string) (*AgentProfileInfo, error)
setExecutionDescriptionFunc func(ctx context.Context, agentExecutionID string, description string) error
}
func (m *mockAgentManager) LaunchAgent(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error) {
@@ -287,7 +287,7 @@ func (m *mockRepository) GetActiveTurnBySessionID(ctx context.Context, sessionID
return nil, nil
}
func (m *mockRepository) UpdateTurn(ctx context.Context, turn *models.Turn) error { return nil }
func (m *mockRepository) CompleteTurn(ctx context.Context, id string) error { return nil }
func (m *mockRepository) CompleteTurn(ctx context.Context, id string) error { return nil }
func (m *mockRepository) CompleteRunningToolCallsForTurn(ctx context.Context, turnID string) (int64, error) {
return 0, nil
}
@@ -882,9 +882,9 @@ func TestShouldUseWorktree(t *testing.T) {
func TestRepositoryCloneURL(t *testing.T) {
tests := []struct {
name string
repo *models.Repository
want string
name string
repo *models.Repository
want string
}{
{
name: "github repo",
@@ -46,13 +46,13 @@ func (h *QueueHandlers) RegisterHandlers(d *ws.Dispatcher) {
// WebSocket Handlers
type wsQueueMessageRequest struct {
SessionID string `json:"session_id"`
TaskID string `json:"task_id"`
Content string `json:"content"`
Model string `json:"model,omitempty"`
PlanMode bool `json:"plan_mode,omitempty"`
SessionID string `json:"session_id"`
TaskID string `json:"task_id"`
Content string `json:"content"`
Model string `json:"model,omitempty"`
PlanMode bool `json:"plan_mode,omitempty"`
Attachments []messagequeue.MessageAttachment `json:"attachments,omitempty"`
UserID string `json:"user_id,omitempty"`
UserID string `json:"user_id,omitempty"`
}
func (h *QueueHandlers) wsQueueMessage(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
@@ -17,10 +17,10 @@ import (
// mockQueueService is a simple mock implementation of QueueService
type mockQueueService struct {
queueMessageFunc func(ctx context.Context, sessionID, taskID, content, model, userID string, planMode bool, attachments []messagequeue.MessageAttachment) (*messagequeue.QueuedMessage, error)
cancelQueuedFunc func(ctx context.Context, sessionID string) (*messagequeue.QueuedMessage, error)
getStatusFunc func(ctx context.Context, sessionID string) *messagequeue.QueueStatus
updateMessageFunc func(ctx context.Context, sessionID, content string) error
queueMessageFunc func(ctx context.Context, sessionID, taskID, content, model, userID string, planMode bool, attachments []messagequeue.MessageAttachment) (*messagequeue.QueuedMessage, error)
cancelQueuedFunc func(ctx context.Context, sessionID string) (*messagequeue.QueuedMessage, error)
getStatusFunc func(ctx context.Context, sessionID string) *messagequeue.QueueStatus
updateMessageFunc func(ctx context.Context, sessionID, content string) error
}
func (m *mockQueueService) QueueMessage(ctx context.Context, sessionID, taskID, content, model, userID string, planMode bool, attachments []messagequeue.MessageAttachment) (*messagequeue.QueuedMessage, error) {
@@ -4,15 +4,15 @@ import "time"
// QueuedMessage represents a message queued for a session
type QueuedMessage struct {
ID string `json:"id"` // Unique queue entry ID
SessionID string `json:"session_id"` // Task session ID
TaskID string `json:"task_id"` // Task ID
Content string `json:"content"` // Message content
Model string `json:"model"` // Optional model override
PlanMode bool `json:"plan_mode"` // Plan mode enabled
Attachments []MessageAttachment `json:"attachments"` // Image attachments
QueuedAt time.Time `json:"queued_at"` // When queued
QueuedBy string `json:"queued_by"` // User ID who queued
ID string `json:"id"` // Unique queue entry ID
SessionID string `json:"session_id"` // Task session ID
TaskID string `json:"task_id"` // Task ID
Content string `json:"content"` // Message content
Model string `json:"model"` // Optional model override
PlanMode bool `json:"plan_mode"` // Plan mode enabled
Attachments []MessageAttachment `json:"attachments"` // Image attachments
QueuedAt time.Time `json:"queued_at"` // When queued
QueuedBy string `json:"queued_by"` // User ID who queued
}
// MessageAttachment represents an attachment (image) in a queued message
@@ -24,6 +24,6 @@ type MessageAttachment struct {
// QueueStatus represents the queue status for a session
type QueueStatus struct {
IsQueued bool `json:"is_queued"` // Whether a message is queued
Message *QueuedMessage `json:"message"` // The queued message (nil if not queued)
IsQueued bool `json:"is_queued"` // Whether a message is queued
Message *QueuedMessage `json:"message"` // The queued message (nil if not queued)
}
@@ -18,11 +18,11 @@ var (
// QueuedTask represents a task in the priority queue
type QueuedTask struct {
TaskID string
Priority int // Higher priority = processed first
QueuedAt time.Time
Task *v1.Task // Full task data
index int // Index in the heap (used by container/heap)
TaskID string
Priority int // Higher priority = processed first
QueuedAt time.Time
Task *v1.Task // Full task data
index int // Index in the heap (used by container/heap)
}
// taskHeap implements heap.Interface for priority queue
@@ -95,10 +95,10 @@ func (q *TaskQueue) Enqueue(task *v1.Task) error {
}
qt := &QueuedTask{
TaskID: task.ID,
Priority: task.Priority,
QueuedAt: time.Now(),
Task: task,
TaskID: task.ID,
Priority: task.Priority,
QueuedAt: time.Now(),
Task: task,
}
heap.Push(&q.heap, qt)
@@ -11,13 +11,13 @@ import (
// createTestTask creates a task for testing with the given parameters
func createTestTask(id string, priority int) *v1.Task {
return &v1.Task{
ID: id,
ID: id,
WorkflowID: "test-wf",
Title: "Test Task " + id,
Priority: priority,
State: v1.TaskStateTODO,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Title: "Test Task " + id,
Priority: priority,
State: v1.TaskStateTODO,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
@@ -160,13 +160,13 @@ func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Lo
func createTestTask(id string, priority int) *v1.Task {
return &v1.Task{
ID: id,
ID: id,
WorkflowID: "test-wf",
Title: "Test Task " + id,
Priority: priority,
State: v1.TaskStateTODO,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Title: "Test Task " + id,
Priority: priority,
State: v1.TaskStateTODO,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
+12 -11
View File
@@ -430,26 +430,27 @@ func (s *Service) Stop() error {
// Recovery Strategy:
//
// 1. Query database for sessions marked as "running" (persisted executor state)
//
// 2. For each session, determine recovery action based on previous state:
//
// Terminal States (Completed/Failed/Cancelled):
// - Clean up stale executor record from database
// - Skip resume (session is already done)
// - Clean up stale executor record from database
// - Skip resume (session is already done)
//
// Never-Started State (Created):
// - Clean up executor record (session never actually ran)
// - Skip resume (nothing to recover)
// - Clean up executor record (session never actually ran)
// - Skip resume (nothing to recover)
//
// Active States (Starting/Running/WaitingForInput):
// - Validate workspace/worktree still exists
// - Launch new agentctl instance
// - Restore session via ACP session/load (if resumable)
// - Transition to WaitingForInput (ready for next prompt)
// - Validate workspace/worktree still exists
// - Launch new agentctl instance
// - Restore session via ACP session/load (if resumable)
// - Transition to WaitingForInput (ready for next prompt)
//
// 3. Handle resume failures gracefully:
// - Mark session as Failed in database
// - Clean up executor record
// - Continue with next session (don't block other recoveries)
// - Mark session as Failed in database
// - Clean up executor record
// - Continue with next session (don't block other recoveries)
//
// Agent Resume Behavior:
//
@@ -884,4 +884,3 @@ func (s *Service) CompleteTask(ctx context.Context, taskID string) error {
func (s *Service) GetQueuedTasks() []*queue.QueuedTask {
return s.queue.List()
}
@@ -37,11 +37,11 @@ func (s *mockSubscription) IsValid() bool {
// mockEventBus implements bus.EventBus for testing
type mockEventBus struct {
subscriptions map[string][]bus.EventHandler
queuedSubs map[string]map[string][]bus.EventHandler
mu sync.RWMutex
connected bool
subscribeErr error
subscriptions map[string][]bus.EventHandler
queuedSubs map[string]map[string][]bus.EventHandler
mu sync.RWMutex
connected bool
subscribeErr error
queueSubscribeErr error
}
+2 -2
View File
@@ -261,8 +261,8 @@ type CreateTaskRequest struct {
Priority int
State *v1.TaskState
Repositories []TaskRepositoryInput
Position int
Metadata map[string]interface{}
Position int
Metadata map[string]interface{}
}
type UpdateTaskRequest struct {
@@ -22,7 +22,6 @@ import (
"github.com/kandev/kandev/internal/task/service"
)
// queryValueTrue is the string "true" used in query parameter comparisons.
const queryValueTrue = "true"
@@ -392,7 +392,7 @@ func (h *TaskHandlers) startAgentForNewTask(
// Launch agent asynchronously so the HTTP request can return immediately.
// The frontend will receive WebSocket updates when the agent actually starts.
executorID := body.ExecutorID // Capture for goroutine
stepID := resolvedStepID // Capture for goroutine
stepID := resolvedStepID // Capture for goroutine
go func() {
// Use a longer timeout to accommodate setup scripts (which can take minutes for npm install, etc.)
startCtx, cancel := context.WithTimeout(context.Background(), constants.AgentLaunchTimeout)

Some files were not shown because too many files have changed in this diff Show More