style: fix format issues (#255)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ apps/backend/mock-agent
|
||||
.next/
|
||||
out/
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.next
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -58,4 +58,3 @@ type EventBus interface {
|
||||
// IsConnected returns connection status
|
||||
IsConnected() bool
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user