Skip to content
Merged

UI #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.19'
go-version: '1.26'

- name: Run tests
run: go test -v ./...
Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go-version: ['1.19', '1.20', '1.21']
go-version: ['1.26']
runs-on: ${{ matrix.os }}

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}

Expand All @@ -39,22 +39,22 @@ jobs:
- name: Run tests
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...

- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.21'
uses: codecov/codecov-action@v3
with:
file: ./coverage.txt
# - name: Upload coverage
# if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.26'
# uses: codecov/codecov-action@v3
# with:
# file: ./coverage.txt

# lint:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# uses: actions/checkout@v5

# - name: Set up Go
# uses: actions/setup-go@v5
# uses: actions/setup-go@v6
# with:
# go-version: '1.21'
# go-version: '1.26'

# - name: Run golangci-lint
# uses: golangci/golangci-lint-action@v3
Expand Down Expand Up @@ -85,12 +85,12 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.21'
go-version: '1.26'

- name: Build
env:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Built-in tools: file operations, shell execution, code search
- Permission confirmation system with risk classification
- Multi-LLM provider support (Anthropic Claude, OpenAI GPT)
- Project context awareness (AICODER.md, Git status, dependencies)
- Project context awareness (.AICODER.md, Git status, dependencies)
- 11 slash commands: /help, /clear, /history, /undo, /diff, /commit, /cost, /model, /config, /init, /exit
- Session management with snapshots and undo
- Token usage tracking and cost estimation
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- **内置工具集**:文件读写编辑、Shell 命令执行、全局代码搜索
- **权限确认机制**:危险操作前要求用户确认,支持黑名单和白名单
- **多 LLM 提供商**:Anthropic Claude、OpenAI GPT(兼容任何 OpenAI 格式端点)
- **项目上下文感知**:自动读取 `AICODER.md`、Git 状态、项目依赖
- **项目上下文感知**:自动读取 `.AICODER.md`、Git 状态、项目依赖
- **斜杠命令**:`/diff`、`/undo`、`/commit`、`/cost` 等 11 个内置命令
- **纯 Go 标准库**:无外部依赖,单二进制,跨平台

Expand Down Expand Up @@ -113,7 +113,7 @@ $ aicoder
| `/cost` | 查看 Token 用量和费用估算 |
| `/model [name]` | 查看或切换 AI 模型 |
| `/config` | 查看当前配置 |
| `/init` | 在当前目录生成 AICODER.md 模板 |
| `/init` | 在当前目录生成 .AICODER.md 模板 |
| `/exit` | 退出程序 |

---
Expand Down Expand Up @@ -143,9 +143,9 @@ $ aicoder

---

## AICODER.md 项目配置
## .AICODER.md 项目配置

在项目根目录创建 `AICODER.md`(或运行 `/init`),AI 会在每次会话开始时自动加载它作为项目级系统提示词:
在项目根目录创建 `.AICODER.md`(或运行 `/init`),AI 会在每次会话开始时自动加载它作为项目级系统提示词:

```markdown
# 项目说明
Expand Down
154 changes: 64 additions & 90 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import (
"strings"
"syscall"

"github.com/chzyer/readline"
"github.com/iminders/aicoder/internal/agent"
"github.com/iminders/aicoder/internal/config"
"github.com/iminders/aicoder/internal/llm"
anthropicprovider "github.com/iminders/aicoder/internal/llm/anthropic"
deepseekprovider "github.com/iminders/aicoder/internal/llm/deepseek"
openaiprovider "github.com/iminders/aicoder/internal/llm/openai"
"github.com/iminders/aicoder/internal/logger"
"github.com/iminders/aicoder/internal/skills"
"github.com/iminders/aicoder/internal/slash"
"github.com/iminders/aicoder/internal/ui"
"github.com/iminders/aicoder/pkg/version"
Expand All @@ -25,6 +26,7 @@ import (
_ "github.com/iminders/aicoder/internal/tools/filesystem"
_ "github.com/iminders/aicoder/internal/tools/search"
_ "github.com/iminders/aicoder/internal/tools/shell"

)

// flags holds CLI flag values.
Expand Down Expand Up @@ -106,6 +108,11 @@ func Execute() {
// Init logger
logger.Init(flags.verbose)

// Load skills (built-ins + user custom)
if err := skills.Load(); err != nil {
logger.Warn("skill load error: %v", err)
}

// Build provider
provider, err := buildProvider(cfg)
if err != nil {
Expand Down Expand Up @@ -170,88 +177,8 @@ func runOneShot(a *agent.Agent, prompt string) {
}

func runInteractive(a *agent.Agent, cfg *config.Config) {
slashHandler := slash.NewHandler(a.Session(), cfg)

// Setup readline with tab completion
completer := readline.NewPrefixCompleter()
for _, cmd := range slash.AllCommands() {
completer.Children = append(completer.Children, readline.PcItem(cmd.Name))
}

rl, err := readline.NewEx(&readline.Config{
Prompt: "\033[1;34m> \033[0m",
HistoryFile: getHistoryFile(),
AutoComplete: completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
// Fallback to basic reader if readline fails
runInteractiveBasic(a, cfg)
return
}
defer rl.Close()

// Setup signal handling for Ctrl+C (interrupt current task, not exit)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)

var cancelCurrent context.CancelFunc

go func() {
for range sigCh {
if cancelCurrent != nil {
fmt.Println("\n\033[33m[任务已中断,输入新的指令继续]\033[0m")
cancelCurrent()
cancelCurrent = nil
}
}
}()

for {
line, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt {
if cancelCurrent != nil {
cancelCurrent()
cancelCurrent = nil
}
continue
} else if err == io.EOF {
fmt.Println("\n再见!")
break
}
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}

// Handle slash commands
if strings.HasPrefix(input, "/") {
handled, shouldExit := slashHandler.Handle(input)
if shouldExit {
fmt.Println("再见!")
return
}
if handled {
continue
}
}

// Run agent
ctx, cancel := context.WithCancel(context.Background())
cancelCurrent = cancel

ui.PrintDivider()
if err := a.Run(ctx, input); err != nil && ctx.Err() == nil {
ui.PrintError(err.Error())
}
cancel()
cancelCurrent = nil
ui.PrintDivider()
}
// Use simple interactive mode (bubbletea TUI conflicts with agent's streaming output)
runInteractiveBasic(a, cfg)
}

func isPipeInput() bool {
Expand All @@ -276,8 +203,15 @@ func buildProvider(cfg *config.Config) (llm.Provider, error) {
return nil, fmt.Errorf("未找到 OpenAI API Key,请设置 OPENAI_API_KEY 环境变量")
}
return openaiprovider.New(apiKey, cfg.BaseURL, cfg.Model), nil
case "deepseek":
// For local deployments, API key is optional
// If baseURL is set to localhost/127.0.0.1, allow empty API key
if apiKey == "" && (cfg.BaseURL == "" || (!strings.Contains(cfg.BaseURL, "localhost") && !strings.Contains(cfg.BaseURL, "127.0.0.1"))) {
return nil, fmt.Errorf("未找到 DeepSeek API Key,请设置 DEEPSEEK_API_KEY 环境变量")
}
return deepseekprovider.New(apiKey, cfg.BaseURL, cfg.Model), nil
default:
return nil, fmt.Errorf("不支持的 provider: %s (支持: anthropic, openai)", cfg.Provider)
return nil, fmt.Errorf("不支持的 provider: %s (支持: anthropic, openai, deepseek)", cfg.Provider)
}
}

Expand Down Expand Up @@ -315,15 +249,38 @@ func runInteractiveBasic(a *agent.Agent, cfg *config.Config) {

for {
fmt.Print("\033[1;34m> \033[0m")
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("\n再见!")

// Read input with multi-line support
// Use Ctrl+D (EOF) to submit, or empty line after content
var inputLines []string
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
if len(inputLines) > 0 {
// Submit accumulated input
break
}
fmt.Println("\n再见!")
return
}
continue
}

trimmed := strings.TrimSpace(line)

// If empty line and we have content, submit
if trimmed == "" && len(inputLines) > 0 {
break
}
continue

// If not empty, accumulate
if trimmed != "" {
inputLines = append(inputLines, line)
}
}
input := strings.TrimSpace(line)

input := strings.TrimSpace(strings.Join(inputLines, ""))
if input == "" {
continue
}
Expand All @@ -336,6 +293,23 @@ func runInteractiveBasic(a *agent.Agent, cfg *config.Config) {
return
}
if handled {
// Check if /skill <n> <prompt> queued a skill run
if a.Session().PendingSkillName != "" {
skillName := a.Session().PendingSkillName
prompt := a.Session().PendingPrompt
a.Session().PendingSkillName = ""
a.Session().PendingPrompt = ""

ctx, cancel := context.WithCancel(context.Background())
cancelCurrent = cancel
ui.PrintDivider()
if err := a.RunWithSkillByName(ctx, prompt, skillName); err != nil && ctx.Err() == nil {
ui.PrintError(err.Error())
}
cancel()
cancelCurrent = nil
ui.PrintDivider()
}
continue
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/PHASE6_7_COMPLETION.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
| `/cost` | ✅ | 展示 Token 消耗和费用估算 |
| `/model [name]` | ✅ | 查看或热切换 AI 模型 |
| `/config [set key value]` | ✅ | 查看或修改配置并持久化 |
| `/init` | ✅ | 生成 AICODER.md 模板 |
| `/init` | ✅ | 生成 .AICODER.md 模板 |
| `/exit`, `/quit`, `/q` | ✅ | 优雅退出程序 |

#### 3. Tab 补全功能 (`internal/slash/completion.go`) 🆕
Expand Down
4 changes: 2 additions & 2 deletions docs/PHASE6_7_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ This document summarizes the implementation of Phase 6 (Slash Commands) and Phas
- Validates values (e.g., provider must be anthropic/openai)
- Persists changes to user config file

10. **`/init`** - Initialize AICODER.md template
- Creates AICODER.md in current directory
10. **`/init`** - Initialize .AICODER.md template
- Creates .AICODER.md in current directory
- Includes sections: Project Description, Code Standards, Common Commands, Notes
- Adds version and timestamp footer

Expand Down
8 changes: 4 additions & 4 deletions docs/arch.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ aicoder/
│ │ ├── collector.go # 上下文收集主入口
│ │ ├── git.go # Git 信息(status/diff/log)
│ │ ├── project.go # 项目语言/依赖检测
│ │ ├── aicoder_md.go # AICODER.md 加载与解析
│ │ ├── aicoder_md.go # .AICODER.md 加载与解析
│ │ └── summarizer.go # 目录结构摘要生成
│ │
│ ├── session/ # 会话管理
Expand Down Expand Up @@ -659,15 +659,15 @@ type FileSnapshot struct {
**实现状态:** ✅ 已完成

**收集的信息:**
- AICODER.md 内容
- .AICODER.md 内容
- Git 状态 (分支、修改、最近提交)
- 项目类型检测 (Go/Node.js/Python/Rust/Java/Ruby)
- 项目根目录

**系统提示词构成:**
```
基础角色定义
+ AICODER.md (项目说明)
+ .AICODER.md (项目说明)
+ 项目环境信息
+ Git 状态
```
Expand Down Expand Up @@ -753,7 +753,7 @@ type FileSnapshot struct {
**待实现:**
- ⏳ 多 Agent 并行任务
- ⏳ Web Dashboard
- ⏳ AICODER.md 模板市场
- ⏳ .AICODER.md 模板市场
- ⏳ VS Code 插件

### 8.4 技术债务和改进空间
Expand Down
Loading
Loading