thatgfsj-code 0.8.1 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ROADMAP.md CHANGED
@@ -2,60 +2,90 @@
2
2
 
3
3
  ---
4
4
 
5
- ## ✅ v0.3.0 - 架构重构 (当前版本)
5
+ ## ✅ v0.9.0 - 稳定版 (当前版本)
6
6
 
7
- ### 核心改进
8
- - [x] 参考 opencode 重构模块化架构
7
+ ### 核心架构
8
+ - [x] Ink (React) TUI 框架
9
9
  - [x] LLM Provider 抽象层 (OpenAI/Anthropic/Gemini)
10
- - [x] 统一 App 核心服务层
11
- - [x] 精简 CLI 入口 (去掉 explain/debug/chat/template)
12
- - [x] 类型系统整理 (types.ts 拆分到各模块)
13
- - [x] 删除未使用模块 (skills, subagent, state, intent)
14
-
15
- ### 新目录结构
16
- ```
17
- src/
18
- ├── cmd/ # CLI 入口
19
- ├── app/ # 核心服务 + Agent Loop
20
- ├── config/ # 配置 + Provider 定义
21
- ├── llm/ # LLM Provider 抽象
22
- ├── session/ # 会话管理
23
- ├── tools/ # 工具系统
24
- ├── tui/ # 终端 UI
25
- ├── mcp/ # MCP 协议
26
- ├── hooks/ # 钩子系统
27
- ├── prompts/ # 提示构建
28
- └── utils/ # 工具函数
29
- ```
10
+ - [x] 15 Provider + 自定义中转站
11
+ - [x] 模块化目录结构 (cmd/app/config/llm/session/tools/tui/skills/hooks/prompts)
12
+
13
+ ### 工具系统
14
+ - [x] file (读/写/列表/删除)
15
+ - [x] shell (执行命令)
16
+ - [x] git (操作)
17
+ - [x] search (搜索)
18
+ - [x] nwt (项目记忆,30天自动归档)
19
+
20
+ ### Skills 系统
21
+ - [x] 16 个内置 Skills (规划/调试/TDD/架构/浏览器等)
22
+ - [x] /skills 命令管理
23
+ - [x] 自动激活
24
+
25
+ ### UI/UX
26
+ - [x] Ink TUI 流式输出
27
+ - [x] Markdown 渲染 (marked-terminal)
28
+ - [x] 工具调用显示 (opencode 风格)
29
+ - [x] 状态栏
30
+ - [x] 命令系统 (/model, /new, /compact, /skills, /mcp, /help)
31
+ - [x] /model 交互式选择框
32
+
33
+ ### 智能功能
34
+ - [x] 自动读取 CLAUDE.md/SKILLS.md (通用路径)
35
+ - [x] NWT 项目历史自动注入系统提示
36
+ - [x] 上下文自动压缩 (超限触发)
37
+ - [x] API 错误检测 (401/403/429)
38
+ - [x] 启动时检测 API Key
39
+
40
+ ### 安装
41
+ - [x] npm install -g thatgfsj-code
42
+ - [x] Windows PowerShell 一键安装
43
+ - [x] Linux/macOS Bash 一键安装
44
+ - [x] 自动下载 Node.js (如果没有)
45
+
46
+ ### 测试通过
47
+ - [x] 简单任务: 读文件 + 回答
48
+ - [x] 中等任务: 修改文件 + 验证
49
+ - [x] 困难任务: 多步骤 + NWT 记录
50
+ - [x] Git + Search + NWT 联合测试
51
+ - [x] 多文件创建和修改
30
52
 
31
53
  ---
32
54
 
33
55
  ## 📋 未来计划
34
56
 
35
- ### v0.4.0 - 增强功能
57
+ ### v1.0.0
36
58
  - [ ] SQLite 会话持久化
37
- - [ ] 命令历史持久化
38
- - [ ] 自定义工具插件加载
39
- - [ ] MCP 服务器配置文件支持
40
-
41
- ### v0.5.0 - TUI 升级
42
- - [ ] Ink/React TUI 替换 readline
43
- - [ ] 分屏显示 (代码 + 对话)
44
- - [ ] Vim 模式编辑器
45
- - [ ] 文件变更预览面板
46
-
47
- ### v1.0.0 - 稳定版
48
- - [ ] LSP 集成
59
+ - [ ] Token 使用量显示
60
+ - [ ] 命令 Tab 补全
49
61
  - [ ] 多会话管理
50
- - [ ] 插件市场
51
- - [ ] 完整测试覆盖
62
+ - [ ] LSP 集成
63
+ - [ ] 图片支持
52
64
 
53
65
  ---
54
66
 
55
- ## 已完成的历史阶段
67
+ ## 历史版本
56
68
 
57
- ### ✅ v0.1.0 - REPL 基础
58
- - 彩色 UI、命令提示符、Ctrl+C 中断、内置命令
69
+ ### ✅ v0.8.0 - 命令系统
70
+ - /model, /new, /compact, /skills, /mcp 命令
71
+
72
+ ### ✅ v0.7.0 - Skills + NWT
73
+ - 16 个内置 Skills, NWT 项目记忆
74
+
75
+ ### ✅ v0.6.0 - Ink TUI
76
+ - React 组件化 UI, Markdown 渲染
77
+
78
+ ### ✅ v0.5.0 - Provider 重写
79
+ - 15 个 Provider, 自定义中转站
80
+
81
+ ### ✅ v0.4.0 - UI 优化
82
+ - 流式输出, 工具调用显示
83
+
84
+ ### ✅ v0.3.0 - 架构重构
85
+ - 模块化目录, Provider 抽象
59
86
 
60
87
  ### ✅ v0.2.0 - 工具 + LLM
61
- - Git/搜索工具、多 Provider 支持、流式输出、意图识别
88
+ - Git/搜索工具, Provider
89
+
90
+ ### ✅ v0.1.0 - REPL 基础
91
+ - 彩色 UI, 命令提示符
package/dist/cmd/index.js CHANGED
@@ -24,7 +24,7 @@ process.on('unhandledRejection', (reason) => {
24
24
  program
25
25
  .name('gfcode')
26
26
  .description('Thatgfsj Code - AI Coding Assistant')
27
- .version('0.8.1')
27
+ .version('0.8.4')
28
28
  .argument('[prompt]', 'Task to execute (omit to start interactive mode)')
29
29
  .option('-m, --model <model>', 'Specify model')
30
30
  .option('-i, --interactive', 'Force interactive mode')
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.tsx"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,KAAgC,MAAM,OAAO,CAAC;AASrD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAG3C,UAAU,KAAK;IACb,GAAG,EAAE,GAAG,CAAC;CACV;AAED,wBAAgB,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,KAAK,qBAoEpC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.tsx"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,KAAgC,MAAM,OAAO,CAAC;AAUrD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAG3C,UAAU,KAAK;IACb,GAAG,EAAE,GAAG,CAAC;CACV;AAED,wBAAgB,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,KAAK,qBA2FpC"}
package/dist/tui/app.js CHANGED
@@ -1,24 +1,40 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** @jsxImportSource react */
3
3
  import { useState, useCallback } from 'react';
4
- import { Box, Text, useStdout } from 'ink';
4
+ import { Box, useStdout } from 'ink';
5
5
  import { Header } from './components/Header.js';
6
6
  import { ChatList } from './components/ChatList.js';
7
7
  import { Thinking } from './components/Thinking.js';
8
8
  import { UserInput } from './components/UserInput.js';
9
9
  import { StatusBar } from './components/StatusBar.js';
10
+ import { ModelSelector } from './components/ModelSelector.js';
10
11
  import { useChat } from './hooks/useChat.js';
11
12
  import { useCommands } from './hooks/useCommands.js';
12
13
  export function TuiApp({ app }) {
13
- const config = app.config.get();
14
14
  const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
15
- const { handleCommand, awaitingInput } = useCommands(app);
15
+ const { handleCommand } = useCommands(app);
16
16
  const [systemMessages, setSystemMessages] = useState([]);
17
+ const [showModelSelector, setShowModelSelector] = useState(false);
17
18
  const { stdout } = useStdout();
18
19
  const terminalWidth = stdout?.columns || 80;
19
20
  const onSubmit = useCallback(async (input) => {
21
+ // If model selector is showing, treat input as custom model name
22
+ if (showModelSelector) {
23
+ app.config.save({ model: input.trim() });
24
+ setShowModelSelector(false);
25
+ setSystemMessages(prev => [
26
+ ...prev,
27
+ { role: 'assistant', content: `Model → ${input.trim()}` },
28
+ ]);
29
+ return;
30
+ }
20
31
  const result = handleCommand(input);
21
32
  if (result.handled) {
33
+ // Special: /model shows selector
34
+ if (input.trim().toLowerCase() === '/model') {
35
+ setShowModelSelector(true);
36
+ return;
37
+ }
22
38
  if (result.output) {
23
39
  setSystemMessages(prev => [
24
40
  ...prev,
@@ -26,29 +42,33 @@ export function TuiApp({ app }) {
26
42
  { role: 'assistant', content: result.output },
27
43
  ]);
28
44
  }
29
- // Handle special actions
30
45
  if (result.action === 'clear') {
31
46
  setSystemMessages([]);
32
47
  }
33
48
  if (result.action === 'reinit') {
34
- // Run init wizard then reload
35
49
  const { WelcomeScreen } = await import('./welcome.js');
36
50
  await WelcomeScreen.interactiveSetup();
37
- // Reload app config
38
51
  const { App: AppClass } = await import('../app/index.js');
39
52
  const newApp = await AppClass.create();
40
53
  Object.assign(app, newApp);
41
54
  setSystemMessages(prev => [
42
55
  ...prev,
43
- { role: 'assistant', content: 'Configuration updated. Model: ' + app.config.get().model },
56
+ { role: 'assistant', content: 'Config updated. Model: ' + app.config.get().model },
44
57
  ]);
45
58
  }
46
59
  return;
47
60
  }
48
61
  await sendMessage(input);
49
- }, [handleCommand, sendMessage, app]);
62
+ }, [handleCommand, sendMessage, app, showModelSelector]);
50
63
  const allMessages = [...systemMessages, ...messages];
51
64
  const activeSkills = app.skills.listActive().map(s => s.id);
52
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Header, { provider: app.config.get().provider, model: app.config.get().model }), _jsx(ChatList, { messages: allMessages, streaming: streaming, streamingToolCalls: streamingToolCalls, width: terminalWidth - 4 }), isThinking && _jsx(Thinking, {}), awaitingInput && (_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: "#F59E0B", children: "\u25B8 " }), _jsx(Text, { color: "#94A3B8", children: awaitingInput.prompt.split('\n').pop() })] })), _jsx(UserInput, { onSubmit: onSubmit, disabled: isThinking }), _jsx(StatusBar, { messageCount: allMessages.length, skills: activeSkills })] }));
65
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Header, { provider: app.config.get().provider, model: app.config.get().model }), _jsx(ChatList, { messages: allMessages, streaming: streaming, streamingToolCalls: streamingToolCalls, width: terminalWidth - 4 }), _jsx(Thinking, { active: isThinking }), showModelSelector ? (_jsx(ModelSelector, { currentModel: app.config.get().model, onSelect: (model) => {
66
+ app.config.save({ model });
67
+ setShowModelSelector(false);
68
+ setSystemMessages(prev => [
69
+ ...prev,
70
+ { role: 'assistant', content: `Model → ${model}` },
71
+ ]);
72
+ }, onCancel: () => setShowModelSelector(false) })) : (_jsx(UserInput, { onSubmit: onSubmit, disabled: isThinking })), _jsx(StatusBar, { messageCount: allMessages.length, skills: activeSkills })] }));
53
73
  }
54
74
  //# sourceMappingURL=app.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/tui/app.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/tui/app.tsx"],"names":[],"mappings":"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"}
@@ -3,6 +3,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from 'react';
4
4
  import { Box, Text } from 'ink';
5
5
  export const Header = React.memo(function Header({ provider, model }) {
6
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { justifyContent: "space-between", width: "100%", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#06B6D4", bold: true, children: " \u26A1 " }), _jsx(Text, { color: "#22D3EE", bold: true, children: "THATGFSJ CODE" }), _jsx(Text, { dimColor: true, children: " v0.8.1" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "#06B6D4", bold: true, children: [" ", provider, " "] }), _jsx(Text, { dimColor: true, children: "/" }), _jsxs(Text, { color: "#22D3EE", children: [" ", model, " "] })] })] }), _jsx(Text, { color: "#374151", children: '─'.repeat(80) })] }));
6
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { justifyContent: "space-between", width: "100%", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#06B6D4", bold: true, children: " \u26A1 " }), _jsx(Text, { color: "#22D3EE", bold: true, children: "THATGFSJ CODE" }), _jsx(Text, { dimColor: true, children: " v0.8.4" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "#06B6D4", bold: true, children: [" ", provider, " "] }), _jsx(Text, { dimColor: true, children: "/" }), _jsxs(Text, { color: "#22D3EE", children: [" ", model, " "] })] })] }), _jsx(Text, { color: "#374151", children: '─'.repeat(80) })] }));
7
7
  });
8
8
  //# sourceMappingURL=Header.js.map
@@ -0,0 +1,10 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ interface Props {
4
+ currentModel: string;
5
+ onSelect: (model: string) => void;
6
+ onCancel: () => void;
7
+ }
8
+ export declare function ModelSelector({ currentModel, onSelect, onCancel }: Props): React.JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=ModelSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelSelector.d.ts","sourceRoot":"","sources":["../../../src/tui/components/ModelSelector.tsx"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAkBD,wBAAgB,aAAa,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,qBAiCxE"}
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** @jsxImportSource react */
3
+ import { useState } from 'react';
4
+ import { Box, Text } from 'ink';
5
+ import SelectInput from 'ink-select-input';
6
+ const COMMON_MODELS = [
7
+ { label: 'deepseek-chat', value: 'deepseek-chat' },
8
+ { label: 'deepseek-reasoner', value: 'deepseek-reasoner' },
9
+ { label: 'gpt-4o', value: 'gpt-4o' },
10
+ { label: 'gpt-4o-mini', value: 'gpt-4o-mini' },
11
+ { label: 'claude-sonnet-4-20250514', value: 'claude-sonnet-4-20250514' },
12
+ { label: 'claude-3-5-haiku-20241022', value: 'claude-3-5-haiku-20241022' },
13
+ { label: 'gemini-2.0-flash', value: 'gemini-2.0-flash' },
14
+ { label: 'gemini-2.5-pro', value: 'gemini-2.5-pro' },
15
+ { label: 'qwen3-235b-a22b', value: 'qwen3-235b-a22b' },
16
+ { label: 'glm-4-flash', value: 'glm-4-flash' },
17
+ { label: 'kimi-k2.6', value: 'kimi-k2.6' },
18
+ { label: 'mimo-v2.5-pro', value: 'mimo-v2.5-pro' },
19
+ { label: '─ type custom name below ─', value: '__custom__' },
20
+ ];
21
+ export function ModelSelector({ currentModel, onSelect, onCancel }) {
22
+ const [customMode, setCustomMode] = useState(false);
23
+ const [customValue, setCustomValue] = useState('');
24
+ if (customMode) {
25
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#06B6D4", bold: true, children: "Enter model name:" }), _jsxs(Box, { children: [_jsx(Text, { color: "#06B6D4", children: "\u276F " }), _jsx(Text, { children: customValue }), _jsx(Text, { color: "#06B6D4", children: "\u2588" })] })] }));
26
+ }
27
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "#06B6D4", bold: true, children: ["Current: ", currentModel] }), _jsx(Text, { dimColor: true, children: "Select model (\u2191\u2193 Enter):" }), _jsx(SelectInput, { items: COMMON_MODELS, onSelect: (item) => {
28
+ if (item.value === '__custom__') {
29
+ setCustomMode(true);
30
+ }
31
+ else {
32
+ onSelect(item.value);
33
+ }
34
+ } })] }));
35
+ }
36
+ //# sourceMappingURL=ModelSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelSelector.js","sourceRoot":"","sources":["../../../src/tui/components/ModelSelector.tsx"],"names":[],"mappings":";AAAA,6BAA6B;AAC7B,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,WAAW,MAAM,kBAAkB,CAAC;AAQ3C,MAAM,aAAa,GAAG;IACpB,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE;IAClD,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IAC1D,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE;IACpC,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;IAC9C,EAAE,KAAK,EAAE,0BAA0B,EAAE,KAAK,EAAE,0BAA0B,EAAE;IACxE,EAAE,KAAK,EAAE,2BAA2B,EAAE,KAAK,EAAE,2BAA2B,EAAE;IAC1E,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IACxD,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACpD,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACtD,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;IAC9C,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE;IAC1C,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE;IAClD,EAAE,KAAK,EAAE,4BAA4B,EAAE,KAAK,EAAE,YAAY,EAAE;CAC7D,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAS;IACvE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAEnD,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,KAAC,IAAI,IAAC,KAAK,EAAC,SAAS,EAAC,IAAI,wCAAyB,EACnD,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,KAAK,EAAC,SAAS,wBAAU,EAC/B,KAAC,IAAI,cAAE,WAAW,GAAQ,EAC1B,KAAC,IAAI,IAAC,KAAK,EAAC,SAAS,uBAAS,IAC1B,IACF,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,MAAC,IAAI,IAAC,KAAK,EAAC,SAAS,EAAC,IAAI,gCAAW,YAAY,IAAQ,EACzD,KAAC,IAAI,IAAC,QAAQ,yDAAgC,EAC9C,KAAC,WAAW,IACV,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;oBACjB,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;wBAChC,aAAa,CAAC,IAAI,CAAC,CAAC;oBACtB,CAAC;yBAAM,CAAC;wBACN,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACvB,CAAC;gBACH,CAAC,GACD,IACE,CACP,CAAC;AACJ,CAAC"}
@@ -6,11 +6,6 @@ interface CommandResult {
6
6
  }
7
7
  export declare function useCommands(app: App): {
8
8
  handleCommand: (input: string) => CommandResult;
9
- awaitingInput: {
10
- type: string;
11
- prompt: string;
12
- handler: (input: string) => CommandResult;
13
- } | null;
14
9
  };
15
10
  export {};
16
11
  //# sourceMappingURL=useCommands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCommands.d.ts","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAE9C,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC7B;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG;2BAOQ,MAAM,KAAG,aAAa;;cALxD,MAAM;gBACJ,MAAM;iBACL,CAAC,KAAK,EAAE,MAAM,KAAK,aAAa;;EA2J5C"}
1
+ {"version":3,"file":"useCommands.d.ts","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAE9C,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC7B;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG;2BAEQ,MAAM,KAAG,aAAa;EA+GjE"}
@@ -1,146 +1,106 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback } from 'react';
2
2
  export function useCommands(app) {
3
- const [awaitingInput, setAwaitingInput] = useState(null);
4
3
  const handleCommand = useCallback((input) => {
5
- // If we're waiting for input from a multi-step command
6
- if (awaitingInput) {
7
- const result = awaitingInput.handler(input);
8
- setAwaitingInput(null);
9
- return result;
10
- }
11
4
  const cmd = input.trim();
12
- const lower = cmd.toLowerCase();
13
- // ── /model ──────────────────────────────────────────
14
- if (lower === '/model' || lower === 'model') {
15
- const current = app.config.get();
16
- const lines = [
17
- `Current: ${current.provider} / ${current.model}`,
18
- '',
19
- 'Enter new model name directly, e.g.:',
20
- ' deepseek-chat',
21
- ' gpt-4o',
22
- ' claude-sonnet-4-20250514',
23
- ' qwen3-235b-a22b',
24
- '',
25
- 'Or type "provider" to change provider.',
26
- ];
27
- setAwaitingInput({
28
- type: 'model',
29
- prompt: lines.join('\n'),
30
- handler: (input) => {
31
- const val = input.trim();
32
- if (!val)
33
- return { handled: true, output: 'Cancelled.' };
34
- if (val.toLowerCase() === 'provider') {
35
- return { handled: true, action: 'reinit' };
36
- }
37
- app.config.save({ model: val });
38
- return { handled: true, output: `Switched to: ${val}` };
39
- },
40
- });
41
- return { handled: true, output: lines.join('\n') };
5
+ const parts = cmd.split(/\s+/);
6
+ const name = parts[0].toLowerCase();
7
+ const arg = parts.slice(1).join(' ');
8
+ // ── /model [name] ───────────────────────────────────
9
+ if (name === '/model') {
10
+ if (!arg) {
11
+ const c = app.config.get();
12
+ return {
13
+ handled: true,
14
+ output: [
15
+ `Current: ${c.provider} / ${c.model}`,
16
+ '',
17
+ 'Usage: /model <name>',
18
+ ' /model deepseek-chat',
19
+ ' /model gpt-4o',
20
+ ' /model claude-sonnet-4-20250514',
21
+ ' /model qwen3-235b-a22b',
22
+ '',
23
+ 'Or: /provider to change provider',
24
+ ].join('\n'),
25
+ };
26
+ }
27
+ app.config.save({ model: arg });
28
+ return { handled: true, output: `Model → ${arg}` };
29
+ }
30
+ // ── /provider ───────────────────────────────────────
31
+ if (name === '/provider') {
32
+ return { handled: true, action: 'reinit' };
42
33
  }
43
34
  // ── /new, /clear ────────────────────────────────────
44
- if (lower === '/new' || lower === '/clear' || lower === 'clear') {
35
+ if (name === '/new' || name === '/clear') {
45
36
  app.session.clear();
46
- return { handled: true, output: 'Session cleared.', action: 'clear' };
37
+ return { handled: true, output: 'New session.', action: 'clear' };
47
38
  }
48
39
  // ── /compact ────────────────────────────────────────
49
- if (lower === '/compact') {
40
+ if (name === '/compact') {
50
41
  const before = app.session.getMessageCount();
51
42
  app.session.truncate();
52
43
  const after = app.session.getMessageCount();
53
44
  return { handled: true, output: `Compacted: ${before} → ${after} messages` };
54
45
  }
55
- // ── /skills ─────────────────────────────────────────
56
- if (lower === '/skills' || lower === 'skills') {
46
+ // ── /skills [id] ────────────────────────────────────
47
+ if (name === '/skills') {
48
+ if (arg) {
49
+ // Toggle specific skill
50
+ if (app.skills.isActive(arg)) {
51
+ app.skills.deactivate(arg);
52
+ return { handled: true, output: `✗ ${arg}` };
53
+ }
54
+ else if (app.skills.activate(arg)) {
55
+ return { handled: true, output: `✓ ${arg}` };
56
+ }
57
+ return { handled: true, output: `Unknown skill: ${arg}` };
58
+ }
57
59
  const all = app.skills.list();
58
60
  const lines = [
59
- 'Skills ([✓] = active):',
61
+ 'Skills:',
60
62
  ...all.map(s => {
61
- const active = app.skills.isActive(s.id) ? '✓' : ' ';
62
- return ` [${active}] ${s.id.padEnd(24)} ${s.description}`;
63
+ const mark = app.skills.isActive(s.id) ? '✓' : '·';
64
+ return ` ${mark} ${s.id}`;
63
65
  }),
64
66
  '',
65
- 'Usage: /skill <id> to toggle',
67
+ 'Usage: /skills <id> to toggle',
66
68
  ];
67
- setAwaitingInput({
68
- type: 'skill',
69
- prompt: lines.join('\n'),
70
- handler: (input) => {
71
- const id = input.trim();
72
- if (!id)
73
- return { handled: true, output: 'Cancelled.' };
74
- if (app.skills.isActive(id)) {
75
- app.skills.deactivate(id);
76
- return { handled: true, output: `Deactivated: ${id}` };
77
- }
78
- else if (app.skills.activate(id)) {
79
- return { handled: true, output: `Activated: ${id}` };
80
- }
81
- return { handled: true, output: `Unknown skill: ${id}` };
82
- },
83
- });
84
69
  return { handled: true, output: lines.join('\n') };
85
70
  }
86
- // ── /skill <id> (direct toggle) ─────────────────────
87
- if (lower.startsWith('/skill ')) {
88
- const id = cmd.slice(7).trim();
89
- if (app.skills.isActive(id)) {
90
- app.skills.deactivate(id);
91
- return { handled: true, output: `Deactivated: ${id}` };
92
- }
93
- else if (app.skills.activate(id)) {
94
- return { handled: true, output: `Activated: ${id}` };
95
- }
96
- return { handled: true, output: `Unknown skill: ${id}` };
97
- }
98
71
  // ── /mcp ────────────────────────────────────────────
99
- if (lower === '/mcp') {
72
+ if (name === '/mcp') {
100
73
  return {
101
74
  handled: true,
102
75
  output: [
103
- 'MCP (Model Context Protocol)',
104
- '',
105
- 'MCP servers allow connecting external tool providers.',
106
- 'To configure, add MCP servers to ~/.thatgfsj/mcp.json:',
76
+ 'MCP: ~/.thatgfsj/mcp.json',
107
77
  '',
108
78
  ' {',
109
79
  ' "servers": {',
110
- ' "my-server": {',
111
- ' "command": "npx",',
112
- ' "args": ["-y", "my-mcp-server"]',
113
- ' }',
80
+ ' "name": { "command": "npx", "args": ["-y", "server"] }',
114
81
  ' }',
115
82
  ' }',
116
- '',
117
- 'Then restart gfcode to load MCP tools.',
118
83
  ].join('\n'),
119
84
  };
120
85
  }
121
86
  // ── /help ───────────────────────────────────────────
122
- if (lower === '/help' || lower === 'help') {
87
+ if (name === '/help' || name === 'help') {
123
88
  return {
124
89
  handled: true,
125
90
  output: [
126
- 'Commands:',
127
- ' /model Switch model',
128
- ' /new, /clear New session',
129
- ' /compact Compress context',
130
- ' /skills Manage skills',
131
- ' /skill <id> Toggle skill',
132
- ' /mcp MCP settings',
133
- ' /help This help',
134
- ' exit Quit',
135
- '',
136
- 'Keyboard:',
137
- ' ↑/↓ History',
138
- ' Ctrl+C Exit',
91
+ '/model <name> Switch model',
92
+ '/provider Change provider',
93
+ '/new New session',
94
+ '/compact Compress context',
95
+ '/skills [id] List/toggle skills',
96
+ '/mcp MCP settings',
97
+ '/help This help',
98
+ 'exit Quit',
139
99
  ].join('\n'),
140
100
  };
141
101
  }
142
102
  return { handled: false };
143
- }, [app, awaitingInput]);
144
- return { handleCommand, awaitingInput };
103
+ }, [app]);
104
+ return { handleCommand };
145
105
  }
146
106
  //# sourceMappingURL=useCommands.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAS9C,MAAM,UAAU,WAAW,CAAC,GAAQ;IAClC,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAIxC,IAAI,CAAC,CAAC;IAEhB,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,KAAa,EAAiB,EAAE;QACjE,uDAAuD;QACvD,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5C,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACvB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAEhC,uDAAuD;QACvD,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;YAEjC,MAAM,KAAK,GAAG;gBACZ,YAAY,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,KAAK,EAAE;gBACjD,EAAE;gBACF,sCAAsC;gBACtC,iBAAiB;gBACjB,UAAU;gBACV,4BAA4B;gBAC5B,mBAAmB;gBACnB,EAAE;gBACF,wCAAwC;aACzC,CAAC;YAEF,gBAAgB,CAAC;gBACf,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;gBACxB,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;oBACzB,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;oBACzB,IAAI,CAAC,GAAG;wBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;oBACzD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;wBACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;oBAC7C,CAAC;oBACD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;oBAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,GAAG,EAAE,EAAE,CAAC;gBAC1D,CAAC;aACF,CAAC,CAAC;YAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YAChE,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACxE,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC7C,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,MAAM,MAAM,KAAK,WAAW,EAAE,CAAC;QAC/E,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9C,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG;gBACZ,wBAAwB;gBACxB,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;oBACb,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;oBACrD,OAAO,MAAM,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC7D,CAAC,CAAC;gBACF,EAAE;gBACF,8BAA8B;aAC/B,CAAC;YAEF,gBAAgB,CAAC;gBACf,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;gBACxB,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;oBACzB,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;oBACxB,IAAI,CAAC,EAAE;wBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;oBACxD,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;wBAC5B,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;wBAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;oBACzD,CAAC;yBAAM,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;wBACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;oBACvD,CAAC;oBACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,CAAC;gBAC3D,CAAC;aACF,CAAC,CAAC;YAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/B,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5B,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;gBAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;YACzD,CAAC;iBAAM,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;gBACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;YACvD,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,CAAC;QAC3D,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YACrB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE;oBACN,8BAA8B;oBAC9B,EAAE;oBACF,uDAAuD;oBACvD,wDAAwD;oBACxD,EAAE;oBACF,KAAK;oBACL,kBAAkB;oBAClB,sBAAsB;oBACtB,2BAA2B;oBAC3B,yCAAyC;oBACzC,SAAS;oBACT,OAAO;oBACP,KAAK;oBACL,EAAE;oBACF,wCAAwC;iBACzC,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QAED,uDAAuD;QACvD,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC1C,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE;oBACN,WAAW;oBACX,gCAAgC;oBAChC,+BAA+B;oBAC/B,oCAAoC;oBACpC,iCAAiC;oBACjC,gCAAgC;oBAChC,gCAAgC;oBAChC,6BAA6B;oBAC7B,wBAAwB;oBACxB,EAAE;oBACF,WAAW;oBACX,2BAA2B;oBAC3B,wBAAwB;iBACzB,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC;IAEzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AASpC,MAAM,UAAU,WAAW,CAAC,GAAQ;IAElC,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,KAAa,EAAiB,EAAE;QACjE,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAErC,uDAAuD;QACvD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtB,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBAC3B,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,MAAM,EAAE;wBACN,YAAY,CAAC,CAAC,QAAQ,MAAM,CAAC,CAAC,KAAK,EAAE;wBACrC,EAAE;wBACF,sBAAsB;wBACtB,wBAAwB;wBACxB,iBAAiB;wBACjB,mCAAmC;wBACnC,0BAA0B;wBAC1B,EAAE;wBACF,kCAAkC;qBACnC,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb,CAAC;YACJ,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,GAAG,EAAE,EAAE,CAAC;QACrD,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YACzB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QAC7C,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACzC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACpE,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC7C,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,MAAM,MAAM,KAAK,WAAW,EAAE,CAAC;QAC/E,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,GAAG,EAAE,CAAC;gBACR,wBAAwB;gBACxB,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC7B,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;oBAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,CAAC;gBAC/C,CAAC;qBAAM,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;oBACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,CAAC;gBAC/C,CAAC;gBACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,GAAG,EAAE,EAAE,CAAC;YAC5D,CAAC;YAED,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG;gBACZ,SAAS;gBACT,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;oBACb,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;oBACnD,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC;gBAC7B,CAAC,CAAC;gBACF,EAAE;gBACF,+BAA+B;aAChC,CAAC;YACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE;oBACN,2BAA2B;oBAC3B,EAAE;oBACF,KAAK;oBACL,kBAAkB;oBAClB,8DAA8D;oBAC9D,OAAO;oBACP,KAAK;iBACN,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACxC,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE;oBACN,+BAA+B;oBAC/B,kCAAkC;oBAClC,8BAA8B;oBAC9B,mCAAmC;oBACnC,qCAAqC;oBACrC,+BAA+B;oBAC/B,4BAA4B;oBAC5B,uBAAuB;iBACxB,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,OAAO,EAAE,aAAa,EAAE,CAAC;AAC3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatgfsj-code",
3
- "version": "0.8.1",
3
+ "version": "0.8.4",
4
4
  "description": "Thatgfsj Code - AI Coding Assistant",
5
5
  "main": "dist/cmd/index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "chalk": "^5.3.0",
27
27
  "commander": "^11.1.0",
28
28
  "ink": "^7.1.0",
29
+ "ink-select-input": "^6.2.0",
29
30
  "ink-spinner": "^5.0.0",
30
31
  "ink-text-input": "^6.0.0",
31
32
  "inquirer": "^13.3.2",
package/src/cmd/index.tsx CHANGED
@@ -28,7 +28,7 @@ process.on('unhandledRejection', (reason) => {
28
28
  program
29
29
  .name('gfcode')
30
30
  .description('Thatgfsj Code - AI Coding Assistant')
31
- .version('0.8.1')
31
+ .version('0.8.4')
32
32
  .argument('[prompt]', 'Task to execute (omit to start interactive mode)')
33
33
  .option('-m, --model <model>', 'Specify model')
34
34
  .option('-i, --interactive', 'Force interactive mode')
package/src/tui/app.tsx CHANGED
@@ -6,6 +6,7 @@ import { ChatList } from './components/ChatList.js';
6
6
  import { Thinking } from './components/Thinking.js';
7
7
  import { UserInput } from './components/UserInput.js';
8
8
  import { StatusBar } from './components/StatusBar.js';
9
+ import { ModelSelector } from './components/ModelSelector.js';
9
10
  import { useChat } from './hooks/useChat.js';
10
11
  import { useCommands } from './hooks/useCommands.js';
11
12
  import type { App } from '../app/index.js';
@@ -16,17 +17,34 @@ interface Props {
16
17
  }
17
18
 
18
19
  export function TuiApp({ app }: Props) {
19
- const config = app.config.get();
20
20
  const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
21
- const { handleCommand, awaitingInput } = useCommands(app);
21
+ const { handleCommand } = useCommands(app);
22
22
  const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
23
+ const [showModelSelector, setShowModelSelector] = useState(false);
23
24
  const { stdout } = useStdout();
24
25
  const terminalWidth = stdout?.columns || 80;
25
26
 
26
27
  const onSubmit = useCallback(async (input: string) => {
28
+ // If model selector is showing, treat input as custom model name
29
+ if (showModelSelector) {
30
+ app.config.save({ model: input.trim() });
31
+ setShowModelSelector(false);
32
+ setSystemMessages(prev => [
33
+ ...prev,
34
+ { role: 'assistant', content: `Model → ${input.trim()}` },
35
+ ]);
36
+ return;
37
+ }
38
+
27
39
  const result = handleCommand(input);
28
40
 
29
41
  if (result.handled) {
42
+ // Special: /model shows selector
43
+ if (input.trim().toLowerCase() === '/model') {
44
+ setShowModelSelector(true);
45
+ return;
46
+ }
47
+
30
48
  if (result.output) {
31
49
  setSystemMessages(prev => [
32
50
  ...prev,
@@ -35,22 +53,19 @@ export function TuiApp({ app }: Props) {
35
53
  ]);
36
54
  }
37
55
 
38
- // Handle special actions
39
56
  if (result.action === 'clear') {
40
57
  setSystemMessages([]);
41
58
  }
42
59
 
43
60
  if (result.action === 'reinit') {
44
- // Run init wizard then reload
45
61
  const { WelcomeScreen } = await import('./welcome.js');
46
62
  await WelcomeScreen.interactiveSetup();
47
- // Reload app config
48
63
  const { App: AppClass } = await import('../app/index.js');
49
64
  const newApp = await AppClass.create();
50
65
  Object.assign(app, newApp);
51
66
  setSystemMessages(prev => [
52
67
  ...prev,
53
- { role: 'assistant', content: 'Configuration updated. Model: ' + app.config.get().model },
68
+ { role: 'assistant', content: 'Config updated. Model: ' + app.config.get().model },
54
69
  ]);
55
70
  }
56
71
 
@@ -58,7 +73,7 @@ export function TuiApp({ app }: Props) {
58
73
  }
59
74
 
60
75
  await sendMessage(input);
61
- }, [handleCommand, sendMessage, app]);
76
+ }, [handleCommand, sendMessage, app, showModelSelector]);
62
77
 
63
78
  const allMessages = [...systemMessages, ...messages];
64
79
  const activeSkills = app.skills.listActive().map(s => s.id);
@@ -72,14 +87,23 @@ export function TuiApp({ app }: Props) {
72
87
  streamingToolCalls={streamingToolCalls}
73
88
  width={terminalWidth - 4}
74
89
  />
75
- {isThinking && <Thinking />}
76
- {awaitingInput && (
77
- <Box paddingLeft={1}>
78
- <Text color="#F59E0B">▸ </Text>
79
- <Text color="#94A3B8">{awaitingInput.prompt.split('\n').pop()}</Text>
80
- </Box>
90
+ <Thinking active={isThinking} />
91
+ {showModelSelector ? (
92
+ <ModelSelector
93
+ currentModel={app.config.get().model}
94
+ onSelect={(model) => {
95
+ app.config.save({ model });
96
+ setShowModelSelector(false);
97
+ setSystemMessages(prev => [
98
+ ...prev,
99
+ { role: 'assistant', content: `Model → ${model}` },
100
+ ]);
101
+ }}
102
+ onCancel={() => setShowModelSelector(false)}
103
+ />
104
+ ) : (
105
+ <UserInput onSubmit={onSubmit} disabled={isThinking} />
81
106
  )}
82
- <UserInput onSubmit={onSubmit} disabled={isThinking} />
83
107
  <StatusBar messageCount={allMessages.length} skills={activeSkills} />
84
108
  </Box>
85
109
  );
@@ -14,7 +14,7 @@ export const Header = React.memo(function Header({ provider, model }: Props) {
14
14
  <Box>
15
15
  <Text color="#06B6D4" bold> ⚡ </Text>
16
16
  <Text color="#22D3EE" bold>THATGFSJ CODE</Text>
17
- <Text dimColor> v0.8.1</Text>
17
+ <Text dimColor> v0.8.4</Text>
18
18
  </Box>
19
19
  <Box>
20
20
  <Text color="#06B6D4" bold> {provider} </Text>
@@ -0,0 +1,61 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import SelectInput from 'ink-select-input';
5
+
6
+ interface Props {
7
+ currentModel: string;
8
+ onSelect: (model: string) => void;
9
+ onCancel: () => void;
10
+ }
11
+
12
+ const COMMON_MODELS = [
13
+ { label: 'deepseek-chat', value: 'deepseek-chat' },
14
+ { label: 'deepseek-reasoner', value: 'deepseek-reasoner' },
15
+ { label: 'gpt-4o', value: 'gpt-4o' },
16
+ { label: 'gpt-4o-mini', value: 'gpt-4o-mini' },
17
+ { label: 'claude-sonnet-4-20250514', value: 'claude-sonnet-4-20250514' },
18
+ { label: 'claude-3-5-haiku-20241022', value: 'claude-3-5-haiku-20241022' },
19
+ { label: 'gemini-2.0-flash', value: 'gemini-2.0-flash' },
20
+ { label: 'gemini-2.5-pro', value: 'gemini-2.5-pro' },
21
+ { label: 'qwen3-235b-a22b', value: 'qwen3-235b-a22b' },
22
+ { label: 'glm-4-flash', value: 'glm-4-flash' },
23
+ { label: 'kimi-k2.6', value: 'kimi-k2.6' },
24
+ { label: 'mimo-v2.5-pro', value: 'mimo-v2.5-pro' },
25
+ { label: '─ type custom name below ─', value: '__custom__' },
26
+ ];
27
+
28
+ export function ModelSelector({ currentModel, onSelect, onCancel }: Props) {
29
+ const [customMode, setCustomMode] = useState(false);
30
+ const [customValue, setCustomValue] = useState('');
31
+
32
+ if (customMode) {
33
+ return (
34
+ <Box flexDirection="column">
35
+ <Text color="#06B6D4" bold>Enter model name:</Text>
36
+ <Box>
37
+ <Text color="#06B6D4">❯ </Text>
38
+ <Text>{customValue}</Text>
39
+ <Text color="#06B6D4">█</Text>
40
+ </Box>
41
+ </Box>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <Box flexDirection="column">
47
+ <Text color="#06B6D4" bold>Current: {currentModel}</Text>
48
+ <Text dimColor>Select model (↑↓ Enter):</Text>
49
+ <SelectInput
50
+ items={COMMON_MODELS}
51
+ onSelect={(item) => {
52
+ if (item.value === '__custom__') {
53
+ setCustomMode(true);
54
+ } else {
55
+ onSelect(item.value);
56
+ }
57
+ }}
58
+ />
59
+ </Box>
60
+ );
61
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback } from 'react';
2
2
  import type { App } from '../../app/index.js';
3
3
 
4
4
  interface CommandResult {
@@ -8,162 +8,116 @@ interface CommandResult {
8
8
  }
9
9
 
10
10
  export function useCommands(app: App) {
11
- const [awaitingInput, setAwaitingInput] = useState<{
12
- type: string;
13
- prompt: string;
14
- handler: (input: string) => CommandResult;
15
- } | null>(null);
16
11
 
17
12
  const handleCommand = useCallback((input: string): CommandResult => {
18
- // If we're waiting for input from a multi-step command
19
- if (awaitingInput) {
20
- const result = awaitingInput.handler(input);
21
- setAwaitingInput(null);
22
- return result;
23
- }
24
-
25
13
  const cmd = input.trim();
26
- const lower = cmd.toLowerCase();
27
-
28
- // ── /model ──────────────────────────────────────────
29
- if (lower === '/model' || lower === 'model') {
30
- const current = app.config.get();
31
-
32
- const lines = [
33
- `Current: ${current.provider} / ${current.model}`,
34
- '',
35
- 'Enter new model name directly, e.g.:',
36
- ' deepseek-chat',
37
- ' gpt-4o',
38
- ' claude-sonnet-4-20250514',
39
- ' qwen3-235b-a22b',
40
- '',
41
- 'Or type "provider" to change provider.',
42
- ];
43
-
44
- setAwaitingInput({
45
- type: 'model',
46
- prompt: lines.join('\n'),
47
- handler: (input: string) => {
48
- const val = input.trim();
49
- if (!val) return { handled: true, output: 'Cancelled.' };
50
- if (val.toLowerCase() === 'provider') {
51
- return { handled: true, action: 'reinit' };
52
- }
53
- app.config.save({ model: val });
54
- return { handled: true, output: `Switched to: ${val}` };
55
- },
56
- });
14
+ const parts = cmd.split(/\s+/);
15
+ const name = parts[0].toLowerCase();
16
+ const arg = parts.slice(1).join(' ');
17
+
18
+ // ── /model [name] ───────────────────────────────────
19
+ if (name === '/model') {
20
+ if (!arg) {
21
+ const c = app.config.get();
22
+ return {
23
+ handled: true,
24
+ output: [
25
+ `Current: ${c.provider} / ${c.model}`,
26
+ '',
27
+ 'Usage: /model <name>',
28
+ ' /model deepseek-chat',
29
+ ' /model gpt-4o',
30
+ ' /model claude-sonnet-4-20250514',
31
+ ' /model qwen3-235b-a22b',
32
+ '',
33
+ 'Or: /provider to change provider',
34
+ ].join('\n'),
35
+ };
36
+ }
37
+ app.config.save({ model: arg });
38
+ return { handled: true, output: `Model → ${arg}` };
39
+ }
57
40
 
58
- return { handled: true, output: lines.join('\n') };
41
+ // ── /provider ───────────────────────────────────────
42
+ if (name === '/provider') {
43
+ return { handled: true, action: 'reinit' };
59
44
  }
60
45
 
61
46
  // ── /new, /clear ────────────────────────────────────
62
- if (lower === '/new' || lower === '/clear' || lower === 'clear') {
47
+ if (name === '/new' || name === '/clear') {
63
48
  app.session.clear();
64
- return { handled: true, output: 'Session cleared.', action: 'clear' };
49
+ return { handled: true, output: 'New session.', action: 'clear' };
65
50
  }
66
51
 
67
52
  // ── /compact ────────────────────────────────────────
68
- if (lower === '/compact') {
53
+ if (name === '/compact') {
69
54
  const before = app.session.getMessageCount();
70
55
  app.session.truncate();
71
56
  const after = app.session.getMessageCount();
72
57
  return { handled: true, output: `Compacted: ${before} → ${after} messages` };
73
58
  }
74
59
 
75
- // ── /skills ─────────────────────────────────────────
76
- if (lower === '/skills' || lower === 'skills') {
60
+ // ── /skills [id] ────────────────────────────────────
61
+ if (name === '/skills') {
62
+ if (arg) {
63
+ // Toggle specific skill
64
+ if (app.skills.isActive(arg)) {
65
+ app.skills.deactivate(arg);
66
+ return { handled: true, output: `✗ ${arg}` };
67
+ } else if (app.skills.activate(arg)) {
68
+ return { handled: true, output: `✓ ${arg}` };
69
+ }
70
+ return { handled: true, output: `Unknown skill: ${arg}` };
71
+ }
72
+
77
73
  const all = app.skills.list();
78
74
  const lines = [
79
- 'Skills ([✓] = active):',
75
+ 'Skills:',
80
76
  ...all.map(s => {
81
- const active = app.skills.isActive(s.id) ? '✓' : ' ';
82
- return ` [${active}] ${s.id.padEnd(24)} ${s.description}`;
77
+ const mark = app.skills.isActive(s.id) ? '✓' : '·';
78
+ return ` ${mark} ${s.id}`;
83
79
  }),
84
80
  '',
85
- 'Usage: /skill <id> to toggle',
81
+ 'Usage: /skills <id> to toggle',
86
82
  ];
87
-
88
- setAwaitingInput({
89
- type: 'skill',
90
- prompt: lines.join('\n'),
91
- handler: (input: string) => {
92
- const id = input.trim();
93
- if (!id) return { handled: true, output: 'Cancelled.' };
94
- if (app.skills.isActive(id)) {
95
- app.skills.deactivate(id);
96
- return { handled: true, output: `Deactivated: ${id}` };
97
- } else if (app.skills.activate(id)) {
98
- return { handled: true, output: `Activated: ${id}` };
99
- }
100
- return { handled: true, output: `Unknown skill: ${id}` };
101
- },
102
- });
103
-
104
83
  return { handled: true, output: lines.join('\n') };
105
84
  }
106
85
 
107
- // ── /skill <id> (direct toggle) ─────────────────────
108
- if (lower.startsWith('/skill ')) {
109
- const id = cmd.slice(7).trim();
110
- if (app.skills.isActive(id)) {
111
- app.skills.deactivate(id);
112
- return { handled: true, output: `Deactivated: ${id}` };
113
- } else if (app.skills.activate(id)) {
114
- return { handled: true, output: `Activated: ${id}` };
115
- }
116
- return { handled: true, output: `Unknown skill: ${id}` };
117
- }
118
-
119
86
  // ── /mcp ────────────────────────────────────────────
120
- if (lower === '/mcp') {
87
+ if (name === '/mcp') {
121
88
  return {
122
89
  handled: true,
123
90
  output: [
124
- 'MCP (Model Context Protocol)',
125
- '',
126
- 'MCP servers allow connecting external tool providers.',
127
- 'To configure, add MCP servers to ~/.thatgfsj/mcp.json:',
91
+ 'MCP: ~/.thatgfsj/mcp.json',
128
92
  '',
129
93
  ' {',
130
94
  ' "servers": {',
131
- ' "my-server": {',
132
- ' "command": "npx",',
133
- ' "args": ["-y", "my-mcp-server"]',
134
- ' }',
95
+ ' "name": { "command": "npx", "args": ["-y", "server"] }',
135
96
  ' }',
136
97
  ' }',
137
- '',
138
- 'Then restart gfcode to load MCP tools.',
139
98
  ].join('\n'),
140
99
  };
141
100
  }
142
101
 
143
102
  // ── /help ───────────────────────────────────────────
144
- if (lower === '/help' || lower === 'help') {
103
+ if (name === '/help' || name === 'help') {
145
104
  return {
146
105
  handled: true,
147
106
  output: [
148
- 'Commands:',
149
- ' /model Switch model',
150
- ' /new, /clear New session',
151
- ' /compact Compress context',
152
- ' /skills Manage skills',
153
- ' /skill <id> Toggle skill',
154
- ' /mcp MCP settings',
155
- ' /help This help',
156
- ' exit Quit',
157
- '',
158
- 'Keyboard:',
159
- ' ↑/↓ History',
160
- ' Ctrl+C Exit',
107
+ '/model <name> Switch model',
108
+ '/provider Change provider',
109
+ '/new New session',
110
+ '/compact Compress context',
111
+ '/skills [id] List/toggle skills',
112
+ '/mcp MCP settings',
113
+ '/help This help',
114
+ 'exit Quit',
161
115
  ].join('\n'),
162
116
  };
163
117
  }
164
118
 
165
119
  return { handled: false };
166
- }, [app, awaitingInput]);
120
+ }, [app]);
167
121
 
168
- return { handleCommand, awaitingInput };
122
+ return { handleCommand };
169
123
  }