Sema Code CLI 是一个基于 sema-core(事件驱动引擎)和 Ink(终端 React)构建的命令行 AI 编程助手,提供 Claude Code 风格的 REPL 交互体验。
技术栈: TypeScript + Ink 7 (React 19) + sema-core + pnpm
flowchart TD
CLI["$ sema-code"] --> IDX["index.ts<br/>render(App, { incrementalRendering })"]
IDX --> APP["App.tsx"]
APP --> BOOT["useBootstrap()"]
BOOT --> LOADING["setState('loading')<br/>显示 '正在初始化...'"]
LOADING --> CORE["initCore(workingDir)<br/>new SemaCore({ stream, thinking, systemPrompt, ... })"]
CORE --> CHK["getModelList()<br/>检查 ~/.sema/model.conf"]
CHK -->|有模型| SESS["core.createSession()"]
CHK -->|无模型| NO_MODEL["setState('ready')<br/>渲染: Welcome + /model 提示"]
NO_MODEL --> USER_MODEL["用户输入 /model"]
USER_MODEL --> MODEL_SCR["setScreen('models')<br/>ModelScreen 配置并保存"]
MODEL_SCR --> BACK["handleBackFromModels()"]
BACK --> SESS
SESS --> POOL["SessionPool.createSession()"]
POOL --> SESS_CREATE["SemaSession.create()"]
SESS_CREATE --> ENGINE_INIT["SemaEngine.createSession()"]
ENGINE_INIT --> HIST["加载历史消息<br/>~/.sema/history/"]
HIST --> PROMPT["构建 system prompt"]
PROMPT --> MODEL_CHK["检查模型配置"]
MODEL_CHK --> READY["emit('session:ready')<br/>{ usage, sessionId, ... }"]
READY --> CHAT["setSession() + setState('ready')<br/>渲染 ChatScreen"]
sequenceDiagram
actor User
participant IA as InputArea
participant CS as ChatScreen
participant SM as SemaEngine
participant LLM as AI 模型
participant UM as useMessages
participant UStr as useStreaming
participant UP as usePermissions
participant UUse as useUsage
User->>IA: 输入消息 按 Enter
IA->>CS: onSubmit(input)
alt /model 命令
CS->>CS: setScreen('models')
else /clear 命令
CS->>SM: processUserInput('/clear')
else 普通输入
CS->>SM: processUserInput(input)
SM->>SM: state='idle' → startQuery()
loop ReAct 循环
SM->>LLM: POST /chat (含 tools 定义)
LLM-->>SM: thinking chunks
SM-->>UStr: message:thinking:chunk
UStr->>UStr: 32ms 节流缓冲
LLM-->>SM: text chunks
SM-->>UStr: message:text:chunk
UStr->>UStr: 32ms 节流缓冲
LLM-->>SM: tool_call
SM-->>UM: tool:execution:start
UM->>UM: 追加 ToolCallCard
SM-->>UP: tool:permission:request
UP->>CS: setState → PermissionConfirm
CS-->>User: (y) agree (a) allow (n) refuse
User->>CS: 按键 y
CS->>SM: respondToToolPermission('agree')
SM->>SM: 执行工具
SM-->>UM: tool:execution:chunk
SM-->>UM: tool:execution:complete
end
SM-->>UM: message:complete
UM->>UM: 追加 AssistantMessage
SM-->>UUse: conversation:usage
UUse->>UUse: 更新 token 计数
SM-->>CS: state:update (idle)
CS->>IA: 显示 "> _"
end
sequenceDiagram
actor User
participant IA as InputArea
participant CS as ChatScreen
participant SM as SemaEngine
participant LLM as AI 模型
User->>IA: 按 Esc
IA->>CS: onInterrupt()
CS->>SM: session.interrupt()
SM->>SM: abortController.abort()
SM->>LLM: 中断当前 HTTP 请求
LLM-->>SM: 请求终止
SM-->>CS: emit('session:interrupted')
SM-->>CS: emit('state:update', 'idle')
CS->>IA: 显示 "> _"
flowchart TD
EXIT["/exit 或 Ctrl+C"] -->|/exit| PROC_EXIT["InputArea → process.exit(0)"]
EXIT -->|Ctrl+C| INK["Ink exitOnCtrlC<br/>触发 unmount"]
INK --> UNMOUNT["React 组件树卸载"]
UNMOUNT --> CLEANUP["所有 hooks useEffect cleanup 执行"]
CLEANUP --> OFF["摘除全部事件监听<br/>useMessages · useStreaming · useUsage<br/>usePermissions · useForms · useTodos · useEvents"]
OFF --> RESTORE["alternateScreen 恢复<br/>终端回到进入前的状态"]
RESTORE --> FINISH["waitUntilExit() resolve<br/>进程退出 (exit code 0)"]
注意: Ctrl+C 退出时,SemaCore.dispose() 不会被调用(process.exit 直接终止进程)。sema-core 引擎在每次会话更新时自动持久化历史到 ~/.sema/history/,所以退出方式不影响数据安全。
graph TB
subgraph Presentation["展示层 (Ink React 组件)"]
App["App.tsx<br/>屏幕路由"]
CS["ChatScreen.tsx<br/>REPL 布局"]
MS["ModelScreen.tsx<br/>模型管理"]
Comps["14 个组件<br/>(MessageList, InputArea 等)"]
end
subgraph State["状态层 (React Hooks)"]
UB["useBootstrap<br/>初始化 + 会话生命周期"]
US["useSession<br/>state:update"]
UM["useMessages<br/>消息数组管理"]
UStr["useStreaming<br/>文本/思考流节流 (32ms)"]
UUse["useUsage<br/>Token 用量"]
UP["usePermissions<br/>权限状态"]
UF["useForms<br/>表单状态"]
UMod["useModels<br/>模型 CRUD"]
UT["useTodos<br/>待办列表"]
UEvt["useEvents<br/>compact + topic 事件"]
end
subgraph Engine["引擎层 (sema-core)"]
SC["SemaCore<br/>进程级 API"]
SS["SemaSession<br/>会话级 API"]
RL["ReAct 循环<br/>LLM ↔ 工具"]
Evt["EventBus<br/>事件总线"]
end
Presentation -->|"useState / useRef<br/>触发重渲染"| State
State -->|"session.on('event')<br/>事件 → 状态"| Engine
Engine -->|"session.processUserInput()<br/>session.respond*()"| State
核心原则: 三层单向依赖 — 组件只消费 Hook 暴露的状态,Hook 只订阅引擎事件,引擎不感知 UI 存在。
sequenceDiagram
actor 用户
participant InputArea as InputArea
participant ChatScreen as ChatScreen
participant Engine as SemaSession (引擎)
participant EventBus as EventBus
participant Hooks as React Hooks
participant Components as Ink 组件
用户->>InputArea: 输入消息,按 Enter
InputArea->>ChatScreen: onSubmit(input)
ChatScreen->>Engine: session.processUserInput(input)
Note over Engine: ReAct 循环:<br/>LLM 查询 → 工具执行 →<br/>权限检查 → 循环
Engine-->>EventBus: message:text:chunk
EventBus-->>Hooks: useMessages 缓冲 (32ms 节流)
Hooks-->>Components: streamingText 更新
Engine-->>EventBus: tool:execution:start
EventBus-->>Hooks: useMessages 追加工具卡片
Hooks-->>Components: ToolCallCard 渲染
Engine-->>EventBus: tool:permission:request
EventBus-->>Hooks: usePermissions 设置 pending
Hooks-->>Components: PermissionConfirm 内联渲染
用户->>Components: 按 y/a/n
Components->>Engine: respondToToolPermission()
Note over Engine: 引擎继续执行
Engine-->>EventBus: message:complete
EventBus-->>Hooks: useMessages 消息定稿
Hooks-->>Components: MessageList 重渲染
Engine-->>EventBus: state:update (idle)
EventBus-->>Hooks: useSession 设为 idle
Hooks-->>Components: InputArea 显示 "> _" 提示
graph TD
App["App<br/>屏幕路由 + 初始化"]
App -->|"screen='chat'"| ChatScreen
App -->|"screen='models'"| ModelScreen
ChatScreen["ChatScreen<br/>组合所有 Hook"]
ChatScreen --> Welcome["WelcomeScreen<br/>Logo + 版本"]
ChatScreen --> ML["MessageList<br/>消息历史"]
ChatScreen --> SB["StatusBar<br/>模型 · 模式 · tokens"]
ChatScreen --> IA["InputArea<br/>REPL 输入 + 自动补全"]
ChatScreen -->|"条件渲染"| PC["PermissionConfirm<br/>内联权限确认"]
ChatScreen -->|"条件渲染"| FI["FormInline<br/>内联表单"]
ChatScreen -->|"条件渲染"| PE["PlanExitInline<br/>Plan 退出确认"]
ML --> UM["UserMessage"]
ML --> AM["AssistantMessage"]
ML --> AM --> MR["MarkdownRenderer<br/>marked → Ink Text"]
ML --> TC["ToolCallCard"]
ML --> AC["AgentTaskCard"]
ModelScreen -->|"view='add'"| SMP["SimpleModelPicker<br/>预设选择 + API Key"]
ModelScreen -->|"view='custom'"| MF["ModelForm<br/>完整 6 字段表单"]
| 事件 | 消费 Hook | UI 效果 |
|---|---|---|
state:update |
useSession |
idle ↔ processing 状态切换 |
session:error |
useSession |
错误信息横幅 |
message:text:chunk |
useMessages |
流式文本追加(32ms 节流) |
message:complete |
useMessages |
消息定稿,清空流式缓冲 |
message:thinking:chunk |
useMessages |
思考过程展示(灰色) |
tool:permission:request |
usePermissions |
内联权限确认 |
tool:execution:start |
useMessages |
工具卡片 "执行中..." |
tool:execution:chunk |
useMessages |
流式工具输出 |
tool:execution:complete |
useMessages |
工具卡片 "完成" |
tool:execution:error |
useMessages |
工具卡片 "错误"(红色) |
task:agent:start/end |
useMessages |
子 Agent 卡片 |
todos:update |
useTodos |
待办列表渲染 |
pick:option:request |
useForms |
FormInline(单选/多选/文本) |
plan:exit:request |
useForms |
PlanExitInline |
conversation:usage |
useUsage |
StatusBar token 计数 |
compact:exec |
useEvents |
压缩通知(token 前后对比) |
topic:update |
useEvents |
会话标题更新 |
session:ready |
useUsage |
初始 token 用量 |
session:interrupted |
useSession / ChatScreen |
InputArea 显示 "> _" |
下图展示了每个源文件之间的 import 引用关系、props 传递方向和事件流向。
graph TB
subgraph Engine["sema-core (外部 npm 包)"]
SC["SemaCore<br/>进程级: 模型/会话/MCP"]
SS["SemaSession<br/>会话级: processUserInput / EventBus"]
end
subgraph Entry["入口层"]
IDX["index.ts<br/>Ink render() 挂载 App"]
CORE["core/initCore.ts<br/>SemaCore 工厂 / initCore()"]
end
subgraph AppLayer["App 层"]
APP["app.tsx<br/>屏幕路由 · 初始化 · 创建会话"]
end
subgraph Screens["Screens 层"]
CHAT["ChatScreen.tsx<br/>聊天主屏 · 组合 hooks + 组件"]
MODEL["ModelScreen.tsx<br/>模型配置 · list/add/custom/edit"]
end
subgraph Hooks["Hooks 桥接层"]
US["useSession<br/>state:update"]
UM["useMessages<br/>message:*:chunk<br/>tool:execution:*"]
UP["usePermissions<br/>tool:permission:request"]
UF["useForms<br/>pick:option:request<br/>plan:exit:request"]
UMod["useModels<br/>模型 CRUD API 调用"]
UT["useTodos<br/>todos:update"]
end
subgraph ChatComps["ChatScreen 子组件"]
WS["WelcomeScreen"]
ML["MessageList"]
SB["StatusBar"]
IA["InputArea"]
PC["PermissionConfirm"]
FI["FormInline"]
PE["PlanExitInline"]
end
subgraph MLComps["MessageList 子组件"]
UserM["UserMessage"]
AsstM["AssistantMessage"]
TC["ToolCallCard"]
AC["AgentTaskCard"]
MD["render/markdown.tsx<br/>marked → Ink Text"]
end
subgraph ModelComps["ModelScreen 子组件"]
SMP["SimpleModelPicker"]
MF["ModelForm"]
end
CORE -->|"import SemaCore"| SC
APP -->|"import { initCore }"| CORE
IDX -->|"import App"| APP
APP -->|"core.createSession()"| SS
APP -->|"props: session"| CHAT
APP -->|"props: core"| MODEL
CHAT -->|"import hooks"| US
CHAT -->|"import hooks"| UM
CHAT -->|"import hooks"| UP
CHAT -->|"import hooks"| UF
CHAT -->|"import hooks"| UT
MODEL -->|"import hook"| UMod
US -.->|"session.on('state:update')"| SS
UM -.->|"session.on('message:*')"| SS
UP -.->|"session.on('tool:permission:')"| SS
UF -.->|"session.on('pick:/plan:')"| SS
UT -.->|"session.on('todos:update')"| SS
UMod -->|"core.addModel() 等"| SC
CHAT -->|"组合渲染"| WS
CHAT -->|"组合渲染"| ML
CHAT -->|"组合渲染"| SB
CHAT -->|"组合渲染"| IA
CHAT -->|"条件渲染"| PC
CHAT -->|"条件渲染"| FI
CHAT -->|"条件渲染"| PE
ML -->|"import"| UserM
ML -->|"import"| AsstM
ML -->|"import"| TC
ML -->|"import"| AC
AsstM -->|"import renderMarkdown"| MD
MODEL -->|"组合渲染"| SMP
MODEL -->|"组合渲染"| MF
SMP -.->|"onSave/onTest 回调"| UMod
MF -.->|"onSave/onTest 回调"| UMod
图例:
- 实线箭头
-->= import 引用 / 组合关系 - 虚线箭头
-.->= 事件订阅 / 回调传递
sequenceDiagram
actor User as 用户
participant IA as InputArea
participant CS as ChatScreen
participant UM as useMessages
participant US as useSession
participant UP as usePermissions
participant UF as useForms
participant SS as SemaSession
participant SC as SemaCore
participant LLM as AI 模型
User->>IA: 输入 "帮我修复 bug"
IA->>CS: onSubmit(input)
CS->>SS: session.processUserInput(input)
SS->>SS: 加载上下文、systemPrompt、customRules
loop ReAct 循环
SS->>LLM: POST /chat (含 tools 定义)
LLM-->>SS: thinking chunks
SS-->>UM: message:thinking:chunk
UM->>UM: 32ms 节流缓冲 → setState
LLM-->>SS: text chunks
SS-->>UM: message:text:chunk
UM->>UM: 32ms 节流缓冲 → setState
LLM-->>SS: tool_call
SS-->>UM: tool:execution:start
UM->>UM: 追加 ToolCallCard (running)
SS-->>UP: tool:permission:request
UP->>CS: setState → PermissionConfirm 渲染
CS-->>User: 显示 "(y) agree (a) allow (n) refuse"
User->>CS: 按键 y
CS->>SS: respondToToolPermission('agree')
SS->>SS: 执行工具
SS-->>UM: tool:execution:chunk
SS-->>UM: tool:execution:complete
UM->>UM: 更新 ToolCallCard (complete)
end
SS-->>UM: message:complete
UM->>UM: flush buffer → 定稿 AssistantMessage
SS-->>US: state:update (idle)
US->>CS: setState → InputArea 显示 "> _"
SS-->>UM: conversation:usage
UM->>CS: setState → StatusBar token 更新
stateDiagram-v2
[*] --> 初始化: 应用启动
初始化 --> 加载中: SemaCore 已创建
加载中 --> 就绪: 初始化完成
就绪 --> 聊天: 有模型 → 创建会话
就绪 --> 聊天无模型: 无模型 → 等待 /model
聊天 --> 模型配置: /model 命令
聊天无模型 --> 模型配置: /model 命令
模型配置 --> 聊天: Esc (已有模型 → 创建会话)
模型配置 --> 聊天无模型: Esc (仍无模型)
聊天 --> [*]: /exit 或 Ctrl+C
聊天无模型 --> [*]: /exit 或 Ctrl+C
核心规则: 应用绝不会自动跳转到模型配置页。无模型时,聊天界面正常渲染并提示输入 /model。用户必须主动输入 /model 来配置模型。
flowchart TD
A["按 a 添加模型"] --> B["SimpleModelPicker<br/>预设选择器"]
B --> C{"选择预设?"}
C -->|"DeepSeek V4"| D1["输入 API Key"]
C -->|"GLM-4 Plus / Flash / Air"| D2["输入 API Key"]
C -->|"自定义模型..."| E["ModelForm<br/>完整 6 字段手动配置"]
D1 --> F["测试连接"]
D2 --> F
E --> F
F -->|"成功"| G["保存 + 创建会话"]
F -->|"失败"| H["显示错误,可重试"]
预设模型预填了所有技术字段(Base URL、最大输出、上下文长度、适配器),用户只需提供 API Key 即可。
- Hook 持有所有状态 — 组件通过 props 接收状态,不自行管理
- 事件 → 状态是单向的 —
session.on('event', data => setState(transform(data))) - 用户操作 → 引擎是单向的 — 组件调用
session.processUserInput()/session.respond*(),引擎通过事件返回结果 - 流式输出用 ref + 节流 —
useRef缓冲累积 chunk,setTimeout(32ms)批量 flush 到 state,避免数千次渲染 - 历史消息避免重渲染 — 已完成的消息用带 key 的
<Box>,仅最新的 streaming 消息高频更新
| 关注点 | 决策 | 原因 |
|---|---|---|
| 终端输入 | 基于 useInput 自定义实现 |
需要斜杠命令着色 + 自动补全下拉 |
| 权限交互 | 内联(非弹窗遮罩) | Claude Code 风格 — (y) agree (a) always allow (n) refuse 在对话流中渲染 |
| 表单交互 | 内联(非弹窗遮罩) | pick:option:request 的 radio/checkbox/text 直接在对话流中渲染 |
| 流式节流 | 32ms ref+buffer | 防止快速 token 生成时每秒数千次渲染 |
| 历史优化 | spacer + flexGrow={1} |
顶部弹性空白区吸收高度变化,消息自然排列 |
| 增量渲染 | incrementalRendering: true |
Ink 7 特性,只重绘变化行而非整个终端 |
| 独立屏幕 | alternateScreen: true |
Ink 7 alt screen buffer,退出时恢复原终端 |
| 状态栏 | 单行 model · mode · tokens · state |
无水平分隔线,最小占用 |
| 品牌色 | #CD4B1A(橙色) |
Claude Code 品牌参考 |
| 模型持久化 | ~/.sema/model.conf 由 sema-core 管理 |
无需额外存储层 |
| 首次体验 | 聊天界面 + /model 提示 |
不强制跳转配置页;用户自然发现 /model 命令 |
| 斜杠命令 | 自动补全下拉 + 青色着色 | /model、/clear、/exit 支持 ↑↓ 选择 |
| Hook 拆分 | useStreaming / useUsage 独立 |
从 useMessages 拆分,职责更清晰 |
| 视图输入 | 各视图独立 useInput({ isActive }) |
ModelScreen 消除巨型 switch |
┌─────────────────────────────────────────────────┐
│ ░▒▓███████▓▒░ ... Sema Code CLI │ ← WelcomeScreen (Logo + 版本 + 模型名 + 工作目录)
│ v0.1.0 Model: deepseek-v4-pro Dir: /proj│
├─────────────────────────────────────────────────┤
│ │
│ > 帮我修复 login.ts 中的 bug │ ← UserMessage (绿色)
│ │
│ ● 我来分析一下这个bug... │ ← AssistantMessage
│ 代码在 src/login.ts 中... │ (marked 渲染的 Markdown)
│ │
│ ● ViewFile(src/login.ts) ✓ │ ← ToolCallCard (完成态)
│─────────────────────────────────────────────────│
│ Thinking... │ ← thinkingText (灰色斜体, 仅 streaming 时可见)
│ Let me check the code flow... │
│ │
│ ● │ ← streamingText (仅 streaming 时可见)
│ I found the issue. The problem is that...█ │
│─────────────────────────────────────────────────│
│ > _ │ ← InputArea (绿色 > 提示符 + 光标)
│ deepseek-v4-pro · Agent · 3.2k/128k tokens │ ← StatusBar
└─────────────────────────────────────────────────┘
布局要点:
<Box flexGrow={1} />弹性 spacer 将内容推到底部- 消息自然溢出时,最旧的内容从顶部滚出
incrementalRendering: true确保只有变化行被重绘,无需 Static 辅助
┌─────────────────────────────────────────────────┐
│ ... 消息历史 ... │
│─────────────────────────────────────────────────│
│ ● RunShell │ ← PermissionConfirm
│ Command: rm -rf node_modules │ 内联渲染,不弹窗
│ ▸ 是否继续? │
│ (y) 同意本次 (a) 始终允许 (n) 拒绝 (Esc) 取消│
│ 等待选择... _ │ ← useInput 拦截键盘
│─────────────────────────────────────────────────│
│ > _ │
│ model · Agent · tokens · idle │
└─────────────────────────────────────────────────┘
触发条件: permissionPending !== null 时条件渲染,显示在消息和状态栏之间。
┌─────────────────────────────────────────────────┐
│ ... 消息历史 ... │
│─────────────────────────────────────────────────│
│ 需要你确认以下配置 │ ← FormInline (intro)
│ │
│ ● 1. 视觉风格 * │ ← 当前焦点问题 (黄色)
│ ○ 1. 极简高级 │ radio / checkbox / select / text
│ ● 2. Apple 风格 │ 数字键选择,Tab 切换题目
│ ○ 3. Material 风格 │
│ │
│ ○ 2. 大概多少页面? │ ← 非焦点问题
│ ___ │ (text 输入)
│ │
│ Tab 切换 · Enter 提交 · Esc 取消 │
│─────────────────────────────────────────────────│
│ > _ │
│ model · Agent · tokens · idle │
└─────────────────────────────────────────────────┘
触发条件: pendingPick !== null 时条件渲染。Tab 键切换焦点题目,Enter 提交全部答案。
┌─────────────────────────────────────────────────┐
│ 模型配置 (Esc 返回) │
│ │
│ ✓ 模型已保存 │ ← status 消息 (绿色)
│ │
│ 已配置模型: │
│ ▶ ● deepseek-v4-pro[deepseek] ← 主模型 │ ← 选中行 (青色)
│ ○ glm-4-plus[zhipu] │
│ ○ glm-4-flash[zhipu] │
│ │
│ a 添加 e 编辑 d 删除 Enter 切换 ↑↓ 选择 Esc 返回│ ← 操作提示
└─────────────────────────────────────────────────┘
交互: useInput({ isActive: view === 'list' }) 独立处理 — ↑↓ 移动选择,Enter 切换模型,a/e/d 快捷键。
┌─────────────────────────────────────────────────┐
│ 添加模型 (Esc 返回) │
│ │
│ 选择模型: │ ← Step 1: pick
│ ▶ DeepSeek V4 (deepseek) │
│ GLM-4 Plus (zhipu) │
│ GLM-4 Flash (zhipu) │
│ GLM-4 Air (zhipu) │
│ 自定义模型... (手动配置全部参数) │
│ │
│ ↑↓ 选择 Enter 确认 Esc 返回 │
│─────────────────────────────────────────────────│
│ Enter 后 ↓ │
│─────────────────────────────────────────────────│
│ 添加模型 (Esc 返回) │
│ │
│ 模型: DeepSeek V4 (deepseek) │ ← Step 2: apikey
│ API Key: sk-abc123█ │
│ │
│ Enter 保存并测试 Esc 返回选择 │
└─────────────────────────────────────────────────┘
交互: SimpleModelPicker 内部有自己的 useInput,两步骤 — 选预设 → 输 API Key → 测试 → 保存。
┌─────────────────────────────────────────────────┐
│ 添加新模型 (Esc 返回) │
│ │
│ ● 提供商: │ ← 当前编辑字段 (黄色 ●)
│ ▶ 1. deepseek 2. anthropic │ 数字键选择
│ 3. openai 4. zhipu 5. custom │
│ │
│ 模型名: deepseek-v4-pro │ ← 非焦点字段
│ Base URL: https://api.deepseek.com/anthropic │
│ API Key: sk-●●●●●●●● │ ← API Key 被遮罩
│ 最大输出: 32000 tokens │
│ 上下文长度: 1000000 tokens │
│ │
│ 适配器: anthropic │ ← 根据提供商自动推断
│ │
│ Tab 切换 · Enter 下一步/保存 · Esc 上一步/返回 │
└─────────────────────────────────────────────────┘
交互: ModelForm 内部有独立的 useInput,6 个字段逐项填写,Tab/Enter 推进,提供商标配预填 Base URL 和适配器。
┌─────────────────────────────────────────────────┐
│ 更新 API Key (Esc 返回) │
│ │
│ 模型: deepseek-v4-pro[deepseek] │
│ │
│ API Key: sk-new-key█ │ ← 直接输入
│ │
│ ✓ 连接成功,保存中... │ ← 测试结果
│ │
│ Enter 保存并测试 Esc 返回 │
└─────────────────────────────────────────────────┘
交互: useInput({ isActive: view === 'edit' }) 处理键盘,输入 API Key → Enter 测试 → 保存 → 自动返回列表。
bin/
└── sema-code # CLI 入口 hashbang 脚本,npm install -g 后暴露为 sema-code 命令
scripts/
├── install.sh # Linux/macOS 一行安装脚本(curl | bash)
└── install.cmd # Windows CMD 安装脚本
src/
├── index.ts # 应用入口:调用 Ink render() 挂载 <App>,等待进程退出
├── app.tsx # 根组件:管理 screen 路由(chat/models)、初始化 SemaCore、创建会话
│
├── core/
│ └── initCore.ts # SemaCore 单例工厂:初始化引擎配置(workingDir、stream、systemPrompt 等),提供 getModelList 辅助函数
│
├── hooks/ # 桥接层:订阅 sema-core 事件总线,转换为 React state
│ ├── useBootstrap.ts # 初始化状态机:创建 SemaCore → 检查模型 → 创建会话,返回全部 App 层状态
│ ├── useSession.ts # 监听 state:update / session:interrupted,暴露 { state, error }
│ ├── useMessages.ts # 消息数组管理:监听 input:processing / message:complete / tool:execution:* / task:agent:*,维护 messages[]
│ ├── useStreaming.ts # 流式节流:监听 message:text:chunk / message:thinking:chunk,32ms ref+buffer 批量刷新,返回 { streamingText, thinkingText }
│ ├── useUsage.ts # Token 用量:监听 conversation:usage / session:ready,返回 { usage }
│ ├── usePermissions.ts # 监听 tool:permission:request,暴露 pending 请求 + respond() 回调
│ ├── useForms.ts # 监听 pick:option:request / plan:exit:request,暴露 pendingPick + pendingPlanExit + respond 回调
│ ├── useModels.ts # 不监听事件,直接调用 SemaCore API 做模型 CRUD:add/del/switch/testConnection
│ ├── useTodos.ts # 监听 todos:update,维护 todo 列表
│ └── useEvents.ts # 监听 compact:exec / topic:update,展示压缩通知和会话标题
│
├── screens/
│ ├── ChatScreen.tsx # 聊天主屏幕:组合 WelcomeScreen + MessageList + StatusBar + InputArea + 条件渲染 PermissionConfirm/FormInline/PlanExitInline
│ └── ModelScreen.tsx # 模型配置屏幕:list / add / custom / edit 四个子视图,单 useInput 驱动全部交互
│
├── components/
│ ├── WelcomeScreen.tsx # 启动画面:ASCII Art Logo(橙色 #CD4B1A)+ 版本号 + 模型名 + 工作目录
│ ├── MessageList.tsx # 消息容器:顶部 spacer(flexGrow 自动收缩)+ thinking 区域 + streaming 区域 + 历史消息列表(column 从上往下排列)
│ ├── UserMessage.tsx # 用户消息:绿色 > content
│ ├── AssistantMessage.tsx # AI 回复:● 前缀 + marked 渲染的 Markdown 内容
│ ├── ToolCallCard.tsx # 工具调用卡片:显示工具名、运行状态(⏳/✓/✗)、流式输出内容
│ ├── AgentTaskCard.tsx # 子 Agent 任务卡片:显示 Agent 类型、状态、指令
│ ├── PermissionConfirm.tsx # 内联权限确认:(y) 同意本次 (a) 始终允许 (n) 拒绝,Esc 取消
│ ├── FormInline.tsx # 内联表单:支持 radio/checkbox/select/text/textarea,Tab 切换题目,Enter 提交
│ ├── PlanExitInline.tsx # 内联 Plan 退出确认:显示 plan 摘要,↑↓ 选择选项
│ ├── StatusBar.tsx # 底部状态栏单行:modelName · Agent · tokens · processing/idle
│ ├── InputArea.tsx # REPL 输入区:> 绿色提示符 + 斜杠命令自动补全 + 输入历史 ↑↓ + Esc 中断
│ ├── ModelForm.tsx # 完整模型表单(6 字段逐项填写):提供商 → 模型名 → Base URL → API Key → 最大输出 → 上下文长度
│ └── SimpleModelPicker.tsx # 简化模型配置:选择预设 → 输入 API Key → 测试连接 → 保存
│
└── render/
└── markdown.tsx # marked.lexer() → Ink <Text>/<Box> 元素树,支持 heading/code/list/blockquote/hr
dist/ # TypeScript 编译输出(tsc → dist/)
package.json # 项目元数据 + 依赖(ink/marked/chalk/sema-core)+ 脚本 + ESM 声明
tsconfig.json # TypeScript 配置:ES2022、NodeNext、strict、jsx react-jsx
pnpm-lock.yaml # pnpm 依赖锁定文件
CLAUDE.md # Claude Code 项目指引:架构、UI 规则、集成要点、Git 规范
ARCHITECTURE.md # 本文件:完整架构设计文档
README.md # 项目 README:安装方式、使用说明、快捷键
.prettierrc # Prettier 格式化配置
.prettierignore # Prettier 忽略规则
| 命令 | 处理位置 | 行为 |
|---|---|---|
/model |
ChatScreen / App |
切换到模型配置页 |
/clear |
ChatScreen → session.processUserInput() |
清空会话(引擎处理) |
/exit |
InputArea |
process.exit(0) |
命令在 InputArea 或 ChatScreen 层面拦截。当用户输入 / 时,自动弹出补全下拉,支持 ↑↓ 选择、Tab 补全,命令文字以青色渲染。
pnpm install # 安装依赖
pnpm dev # 开发模式(tsx 热重载)
pnpm build # TypeScript 编译到 dist/
pnpm start # 运行编译产物
pnpm tsc --noEmit # 仅类型检查