新增: u-tabs 初始版本

Go TUI 项目启动器,基于 bubbletea v2 + lipgloss v2。
支持分组 Tab、多选启动、编号跳转、Windows Terminal 集成。
This commit is contained in:
2026-05-16 21:01:03 +08:00
commit a027fe1703
12 changed files with 1356 additions and 0 deletions

73
docs/00-文档目录.md Normal file
View File

@@ -0,0 +1,73 @@
# u-tabs 文档索引
> 最后更新2026-05-15 | 项目状态:活跃
## 文档列表
| 文档 | 说明 |
|------|------|
| [架构设计](./02-技术文档/架构设计.md) | 项目架构、数据模型、UI布局、交互设计、启动逻辑 |
## 快速开始
```bash
cd u-tabs && go run main.go
```
## 配置
工作空间数据从 YAML 配置加载,不硬编码。
```
优先级: ~/.u-tabs/config.yaml > exe同目录/config.yaml > 内置默认值(workspace.go)
```
示例文件:`config.example.yaml`
## 项目结构
```
u-tabs/
├── main.go # 入口: tea.NewProgram → p.Run()
├── go.mod # Go 1.26, Bubbletea v2
├── config.example.yaml # 配置示例
├── internal/
│ ├── app.go # 主 Model: 渲染 + 键盘交互 + 启动逻辑
│ ├── config.go # YAML 配置加载 + Config→内部结构转换
│ ├── workspace.go # Workspace/Group 数据结构 + 内置默认值 + wsByNum 索引
│ └── style/
│ └── style.go # Tokyo Night 主题 + GroupStyles
└── docs/
├── 00-文档目录.md # 本文件
└── 02-技术文档/
└── 架构设计.md # 完整架构文档
```
## 核心依赖
| 依赖 | 版本 | 用途 |
|------|------|------|
| charm.land/bubbletea/v2 | v2.0.2 | TUI 框架 (Elm 架构) |
| charm.land/lipgloss/v2 | v2.0.2 | 终端样式/布局 |
| gopkg.in/yaml.v3 | v3.0.1 | 配置文件解析 |
## 交互
| 按键 | 功能 |
|------|------|
| j/k / ↑↓ | 当前分组内移动光标 |
| Tab / ←→ | 切换分组 |
| 1-4 | 快速跳转分组 |
| Enter | 启动选中项wt.exe 新标签页) |
| Space | 切换多选标记,再 Enter 批量启动 |
| 数字键 | 追加到输入缓冲,按编号启动 |
| c | 复制启动命令到剪贴板 |
| q / Ctrl+C | 退出 |
## 启动机制
```
u-tabs → wt.exe → pwsh -NoExit -EncodedCommand → claude --name xxx (交互模式)
↑ ↑
Windows Terminal UTF-16LE → Base64 编码的 PS 脚本
```

View File

@@ -0,0 +1,222 @@
# u-tabs 架构设计
> 最后更新2026-05-15 | 基于 u-tabs v0.3.0
## 1. 项目概述
u-tabs 是一个 TUI 工作空间启动器,用 Bubble Tea v2 实现。管理多个工作空间YAML 配置驱动),通过 Windows Terminal + Claude Code 标签页启动。
**技术栈**: Go 1.26 / BubbleTea v2 / LipGloss v2
## 2. 架构
```
main.go → Model (app.go)
├── 分组 Tab 栏
├── 左侧项目列表
├── 右侧详情面板
└── Enter → wt.exe → claude
```
## 3. 配置系统
### 3.1 加载优先级
```
~/.u-tabs/config.yaml → exe同目录/config.yaml → 内置默认值 (workspace.go)
```
### 3.2 YAML 结构
```yaml
groups:
- label: CORE # 分组标签,决定编号基数
desc: 核心业务
items:
- title: flux # 短名
prompt: 描述 # AI prompt
tech: 技术栈 # 精确到版本
deploy: 部署 # 服务器/端口/域名
dir: 目录路径 # 启动目录
```
### 3.3 编号规则
`GroupConfig.Base` 字段决定,若未设置默认从 0 开始。
### 3.4 数据转换
`Config.ToInternal()` 将 YAML 配置转为运行时结构,自动生成:
- `Index`: 全局索引 (0, 1, 2...)
- `N`: 编号 (base + item索引)
- `Group`: 继承父级 label
- `Dir`: `~` 展开为用户主目录
## 4. 数据模型
### 4.1 Workspace
```go
type Workspace struct {
Index int // 全局索引
N int // 编号
Title string // 短名
Prompt string // 描述
Tech string // 技术栈
Deploy string // 部署情况
Dir string // 目录路径
Group string // 分组标签
}
```
### 4.2 Group
```go
type Group struct {
Label string
Desc string
}
```
### 4.3 运行时索引
| 变量 | 类型 | 用途 |
|------|------|------|
| `Groups` | `[]Group` | 分组定义 |
| `AllWorkspaces` | `[]Workspace` | 全部工作空间 |
| `wsByNum` | `map[int]*Workspace` | 编号→Workspace O(1) 索引 |
## 5. 主模型 (app.go)
### 5.1 Model
```go
type Model struct {
activeGroup int // 当前分组索引
cursor int // 分组内光标
selected map[int]bool // 多选标记
inputBuf string // 数字快捷输入缓冲
width, height int
launched string // 启动提示
}
```
### 5.2 Update 消息路由
```
KeyPressMsg
├── q, ctrl+c → tea.Quit
├── tab, right, l → activeGroup++ → cursor=0
├── shift+tab, left, h → activeGroup--
├── 1, 2, 3, 4 → 跳转分组
├── up, k → cursor--
├── down, j → cursor++
├── Space → toggleMultiSelect
├── enter → inputBuf非空? launchByInput() : launchSelected()
├── c → copyCommand()
└── [0-9] → 追加到 inputBuf
```
### 5.3 View 渲染
```
View()
└── 启动器模式:
├─ 第1行: "u-tabs"标题 + 分组 Tab 栏
├─ 分隔线
├─ 左右布局 (JoinHorizontal):
│ ├── 左侧列表: 编号 + 标题 + 描述 (CJK安全截断)
│ └── 右侧详情: 目录/编号/描述/技术/部署/命令预览
├─ 输入缓冲提示
├─ 启动成功提示
└─ 帮助栏 (快捷键高亮)
```
### 5.4 CJK 安全截断
`truncateByWidth()` 按 rune 显示宽度截断,中文=2宽英文=1宽不会切断多字节字符
```go
func truncateByWidth(s string, maxW int) string {
w := 0
for i := 0; i < len(s); {
_, size := utf8.DecodeRuneInString(s[i:])
rw := runeWidth(rune(s[i]))
if w+rw > maxW { return s[:i] }
w += rw
i += size
}
return s
}
```
## 6. 启动逻辑
### 6.1 流程
```
用户按 Enter
buildLaunchScript(ws) → PS 脚本
encodePSCommand(script) → UTF-16LE → Base64
exec.Command("wt.exe", "-w", "0",
"-d", ws.Dir,
"--tabColor", randomColor,
"pwsh", "-NoExit", "-EncodedCommand", encoded).Start()
Windows Terminal 新标签页 → claude 交互模式
```
### 6.2 PS 脚本模板
```powershell
$Host.UI.RawUI.WindowTitle = "{Title}"
Write-Host "=== {Title} ===" -ForegroundColor Cyan
Write-Host "Prompt: {Prompt}" -ForegroundColor Yellow
cd "{Dir}"
claude --name "{Title}" --permission-mode bypassPermissions
```
### 6.3 UTF-16LE 编码
```go
func encodePSCommand(script string) string {
u16 := utf16.Encode([]rune(script))
b := make([]byte, len(u16)*2)
for i, r := range u16 {
b[i*2] = byte(r)
b[i*2+1] = byte(r >> 8)
}
return base64.StdEncoding.EncodeToString(b)
}
```
## 7. 样式系统 (Tokyo Night 主题)
### 7.1 色板
| 常量 | Hex | 用途 |
|------|-----|------|
| BgDark | #1a1b26 | 深底色 |
| BgPanel | #292e42 | 面板/边框/分隔线 |
| Dim | #565f89 | 次要文字 |
| Fg | #a9b1d6 | 正文 |
| Bright | #c0caf5 | 高亮文字 |
| Accent | #7aa2f7 | 主强调 (蓝色) |
| Success | #9ece6a | 绿色 (部署/标记) |
| Warning | #e0af68 | 黄色 (输入提示) |
| Cyan | #7dcfff | 青色 (编号/详情) |
| Purple | #bb9af7 | 紫色 (选中行) |
| Red | #f7768e | 红色 (CORE分组) |
### 7.2 分组颜色
| 分组 | Hex |
|------|-----|
| CORE | #f7768e |
| LAB | #9ece6a |
| TOOLS | #e0af68 |
| ME | #bb9af7 |
| TEMP | #7dcfff |

View File

@@ -0,0 +1,245 @@
# u-tabs 历史会话功能 — 需求备忘录
> 日期: 2026-05-16 | 状态: 分析完成
---
## 1. 需求概述
在 u-tabs 最后一个 Tab 新增「历史会话」功能,浏览和管理 Claude Code 所有项目的历史会话记录。
## 2. 核心需求
### 2.1 新增 HISTORY Tab
- 作为最后一个 Tab 追加到现有 Tab 栏CORE / LAB / TOOLS / ME / **HISTORY**
- 切换到该 Tab 时展示三栏布局(其他 Tab 保持原有两栏不变)
### 2.2 三栏布局
```
┌──────────────┬────────────────────┬──────────────────────────┐
│ 项目目录 │ 会话列表 │ 会话详情 │
│ │ │ │
│ ▸ E:/wk-flux │ ▸ 05-16 01:27 │ 标题: 龙享花API对接 │
│ E:/wk-lab │ 05-15 23:01 │ 时间: 05-16 01:27 │
│ E:/wk-oth │ 05-14 15:30 │ 目录: E:/wk-flux │
│ E:/wk-abc │ │ 消息: 94条 │
│ ... │ │ 摘要: 整理龙享花平台API │
│ │ │ 文档并编写测试代码... │
└──────────────┴────────────────────┴──────────────────────────┘
```
| 栏位 | 内容 | 交互 |
|------|------|------|
| 左栏 | 按 `cwd` 分组的项目目录列表 | 上下键切换目录 |
| 中栏 | 当前目录下的会话列表(时间+标题) | 上下键切换会话 |
| 右栏 | 选中会话的详情(标题/时间/消息数/AI摘要 | 只读展示 |
### 2.3 恢复会话
-`Enter` 在对应项目目录下执行 `claude -r <session-id>` 恢复会话
- 通过 `wt.exe` 打开新 Tab 执行(与现有启动逻辑一致)
### 2.4 AI 摘要生成
- 会话标题和摘要通过 AI 分析生成(`claude -p` 非交互模式)
- 生成结果缓存到本地(`~/.u-tabs/session-cache.json`),避免重复分析
- 分层策略:
1. 优先用已有的 `awaySummary`Claude 自动生成的离开摘要)
2. 次选 `customTitle` + 首条用户消息 + 消息数
3. 用户按 `s` 键触发 AI 按需生成,结果缓存
---
## 3. 数据源分析
### 3.1 存储位置
```
~/.claude/projects/<编码目录>/<session-uuid>.jsonl
```
编码规则:`E:\wk-lab\u-tabs``E--wk-lab-u-tabs`
### 3.2 JSONL 关键 entry 类型
| type | 关键字段 | 用途 |
|------|----------|------|
| `custom-title` | `customTitle` | 会话标题 |
| `user` | `message.content` | 用户消息string 或 text block 数组) |
| `assistant` | `message.content` | 助手回复 |
| `system` (subtype=`away_summary`) | `content` | AI 自动生成的离开摘要 |
| 大多数 entry | `cwd`, `timestamp`, `sessionId`, `gitBranch` | 通用元数据 |
### 3.3 扫描策略
**流式扫描,不全量加载:**
- `bufio.Scanner` 逐行读取,`bytes.Contains` 预过滤避免无用 JSON 解析
- 超过 2000 行的文件提前终止(元数据通常在前 50-100 行)
- 缓存机制:`~/.u-tabs/session-cache.json` 记录每个 session 的 modTime + 元数据
- 只重新读取 modTime 变化的文件
- 后续启动从缓存加载,接近即时完成
---
## 4. 技术方案
### 4.1 现有代码结构
```
internal/
├── app.go # Model, Update, View, 启动逻辑 (405行)
├── workspace.go # Workspace/Group 结构体, 运行时状态
├── config.go # YAML 配置加载
└── style/style.go # Tokyo Night 样式
```
### 4.2 文件变更清单
| 文件 | 操作 | 职责 |
|------|------|------|
| `internal/history.go` | **新建** | Session/ProjectDir/HistoryState 结构体、JSONL 扫描器、缓存、三栏渲染、按键处理、会话恢复 |
| `internal/app.go` | **修改** | Model 增加 `history` 字段、Update/View 分支 HISTORY 逻辑、Tab 栏追加 HISTORY、ScanCompleteMsg 处理 |
| `internal/style/style.go` | **修改** | GroupStyles 增加 HISTORY、会话列表/详情专用样式 |
| `internal/workspace.go` | 不变 | — |
| `internal/config.go` | 不变 | — |
| `main.go` | 不变 | — |
### 4.3 核心结构体
```go
// Session — 单个会话的元数据
type Session struct {
ID string // UUID (文件名)
CustomTitle string // customTitle 字段
Cwd string // 实际工作目录
StartTime time.Time // 首条 timestamp
EndTime time.Time // 末条 timestamp
MsgCount int // user + assistant 行数
FirstMsg string // 首条用户消息 (截断)
AwaySummary string // away_summary 系统摘要
}
// ProjectDir — 按目录分组的会话
type ProjectDir struct {
Dir string // 完整路径
Sessions []*Session // 按时间倒序
}
// HistoryState — HISTORY Tab 视图状态
type HistoryState struct {
Projects []*ProjectDir
DirCursor int // 左栏光标
SessCursor int // 中栏光标
FocusPanel int // 0=左栏 1=中栏
Loaded bool
Scanning bool
}
```
### 4.4 Model 修改
```go
type Model struct {
// ... 现有字段不变 ...
history HistoryState // 新增HISTORY Tab 状态
}
```
HISTORY Tab 判断:`m.activeGroup == len(Groups)`(最后一个位置)。
### 4.5 Tab 切换改动
- 现有:`% len(Groups)` 循环
- 改为:`% (len(Groups) + 1)` 包含 HISTORY
- 新增 `5` 键直接跳转 HISTORY
### 4.6 三栏渲染
```
可用宽度 = terminal width - 2 (分隔符)
左栏 (目录): 20% 宽度, 最小 20 字符
中栏 (会话): 40% 宽度, 最小 30 字符
右栏 (详情): 剩余宽度, 最小 30 字符
```
### 4.7 异步扫描
```go
type ScanCompleteMsg struct {
Projects []*ProjectDir
}
// 首次进入 HISTORY Tab 时触发
func ScanSessionsCmd() tea.Cmd {
return func() tea.Msg {
return ScanCompleteMsg{Projects: scanAllProjects()}
}
}
```
扫描期间显示 "scanning..." 占位。
### 4.8 会话恢复
复用现有 `encodePSCommand` + `wt.exe` 模式,改用 `claude -r <session-id>`:
```go
func resumeSession(s *Session) {
script := fmt.Sprintf(`cd "%s"; claude -r %s`, s.Cwd, s.ID)
encoded := encodePSCommand(script)
exec.Command("wt.exe", "-w", "0", "-d", s.Cwd,
"--tabColor", randomColor,
"pwsh", "-NoExit", "-EncodedCommand", encoded).Start()
}
```
### 4.9 HISTORY Tab 按键
| 键 | 动作 |
|----|------|
| `j`/`down` | 当前面板光标下移 |
| `k`/`up` | 当前面板光标上移 |
| `tab`/`right`/`l` | 焦点切到中栏 |
| `shift+tab`/`left`/`h` | 焦点切到左栏 |
| `enter` | 恢复选中会话 |
| `s` | AI 生成摘要(按需) |
| `5` | 从其他 Tab 跳转到 HISTORY |
| `q`/`ctrl+c` | 退出 |
---
## 5. 实现阶段
| 阶段 | 内容 | 涉及文件 |
|------|------|----------|
| P1 数据层 | Session 结构体 + JSONL 流式扫描器 + 缓存 | `history.go` |
| P2 集成 | Model 扩展 + Update 分支 + Tab 栏扩展 | `app.go` |
| P3 渲染 | 三栏布局 + 目录/会话/详情面板 | `history.go` |
| P4 动作 | 会话恢复 + AI 摘要按需生成 | `history.go`, `app.go` |
| P5 样式 | HISTORY Tab 样式 + 会话专用样式 | `style.go` |
---
## 6. 风险与对策
| 风险 | 对策 |
|------|------|
| 大文件 (1MB+) 扫描慢 | 流式扫描 + 2000 行截断 + modTime 缓存 |
| 项目目录过多 | 左栏滚动显示,按最近活跃排序 |
| `claude -p` 生成摘要慢 | 按需触发(`s` 键),非自动;结果缓存 |
| 窄终端三栏挤压 | 定义面板最小宽度,不足时降级为两栏 |
---
## 7. 验证标准
1. HISTORY Tab 出现在 Tab 栏末尾,样式与其他 Tab 一致
2. 左栏列出所有有会话的项目目录,按路径排序
3. 选中目录后中栏显示该目录下所有会话,按时间倒序
4. 选中会话后右栏显示完整详情
5. `Enter` 键在正确目录下通过 `wt.exe` 恢复会话
6. `s` 键触发 AI 摘要生成,结果写入缓存
7. 其他 Tab 功能不受影响
8. 首次进入扫描显示 loading二次进入从缓存加载