thatgfsj-code 3.0.1 → 3.0.3

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.
Files changed (55) hide show
  1. package/dist/app/index.d.ts +6 -0
  2. package/dist/app/index.d.ts.map +1 -1
  3. package/dist/app/index.js +8 -0
  4. package/dist/app/index.js.map +1 -1
  5. package/dist/cache/index.d.ts +1 -1
  6. package/dist/cache/index.d.ts.map +1 -1
  7. package/dist/cache/index.js +1 -1
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/cache/smartModel.d.ts +68 -20
  10. package/dist/cache/smartModel.d.ts.map +1 -1
  11. package/dist/cache/smartModel.js +98 -20
  12. package/dist/cache/smartModel.js.map +1 -1
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/index.js +6 -4
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/types.d.ts +5 -1
  17. package/dist/config/types.d.ts.map +1 -1
  18. package/dist/llm/anthropic.d.ts +17 -0
  19. package/dist/llm/anthropic.d.ts.map +1 -1
  20. package/dist/llm/anthropic.js +25 -1
  21. package/dist/llm/anthropic.js.map +1 -1
  22. package/dist/llm/index.d.ts +9 -0
  23. package/dist/llm/index.d.ts.map +1 -1
  24. package/dist/llm/index.js +36 -1
  25. package/dist/llm/index.js.map +1 -1
  26. package/dist/llm/provider.d.ts +6 -1
  27. package/dist/llm/provider.d.ts.map +1 -1
  28. package/dist/tui/app.d.ts.map +1 -1
  29. package/dist/tui/app.js +17 -44
  30. package/dist/tui/app.js.map +1 -1
  31. package/dist/tui/components/Header.d.ts +7 -0
  32. package/dist/tui/components/Header.d.ts.map +1 -1
  33. package/dist/tui/components/Header.js +2 -2
  34. package/dist/tui/components/Header.js.map +1 -1
  35. package/dist/tui/components/InitWizard.d.ts +18 -9
  36. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  37. package/dist/tui/components/InitWizard.js +76 -52
  38. package/dist/tui/components/InitWizard.js.map +1 -1
  39. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  40. package/dist/tui/hooks/useCommands.js +38 -0
  41. package/dist/tui/hooks/useCommands.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/app/index.ts +8 -0
  44. package/src/cache/index.ts +7 -1
  45. package/src/cache/smartModel.ts +113 -20
  46. package/src/config/index.ts +6 -4
  47. package/src/config/types.ts +5 -1
  48. package/src/llm/anthropic.ts +26 -1
  49. package/src/llm/index.ts +38 -1
  50. package/src/llm/provider.ts +6 -1
  51. package/src/tui/app.tsx +15 -56
  52. package/src/tui/components/Header.tsx +12 -2
  53. package/src/tui/components/InitWizard.tsx +103 -69
  54. package/src/tui/hooks/useCommands.ts +39 -0
  55. package/tests/cache/smartModel.test.ts +112 -0
@@ -12,6 +12,8 @@ const CMD_ALIASES = {
12
12
  '/服务商': '/provider',
13
13
  '/思考': '/thinking',
14
14
  '/缓存': '/cache',
15
+ '/ttl': '/ttl',
16
+ '/TTL': '/ttl',
15
17
  };
16
18
  export const COMMAND_LIST = [
17
19
  { name: '/模型', desc: '切换模型' },
@@ -19,6 +21,7 @@ export const COMMAND_LIST = [
19
21
  { name: '/新建', desc: '新建会话' },
20
22
  { name: '/压缩', desc: '压缩上下文' },
21
23
  { name: '/缓存', desc: '缓存命中率' },
24
+ { name: '/ttl', desc: '查看/设置 Cache TTL' },
22
25
  { name: '/技能', desc: '管理技能' },
23
26
  { name: '/mcp', desc: 'MCP 设置' },
24
27
  { name: '/帮助', desc: '查看帮助' },
@@ -124,6 +127,41 @@ export function useCommands(app) {
124
127
  lines.push('/cache reset · 重置统计');
125
128
  return { handled: true, output: lines.join('\n') };
126
129
  }
130
+ // v3.0.3: /ttl — view or pin the cache TTL.
131
+ //
132
+ // /ttl show current effective TTL + decision reason
133
+ // /ttl 5m|1h pin TTL for this session (sticky, no cache reset)
134
+ // /ttl auto resume auto-decision (clears the pin)
135
+ if (name === '/ttl') {
136
+ const configTtl = app.config.get().cache?.ttl;
137
+ const resolved = app.resolvedTtl;
138
+ if (arg === 'auto') {
139
+ app.config.save({ cache: { ...app.config.get().cache, ttl: 'auto' } });
140
+ app.resolvedTtl = null;
141
+ return { handled: true, output: '✓ Cache TTL 已重置为自动(下一轮重新评估)' };
142
+ }
143
+ if (arg === '5m' || arg === '1h') {
144
+ app.config.save({ cache: { ...app.config.get().cache, ttl: arg } });
145
+ app.resolvedTtl = arg;
146
+ return { handled: true, output: `✓ Cache TTL 已固定为 ${arg}(首次请求会重建缓存)` };
147
+ }
148
+ // No arg: show current state
149
+ const lines = [
150
+ '⏱ Cache TTL',
151
+ '─────────────────────────',
152
+ ` 配置: ${configTtl ?? 'auto'}`,
153
+ ` 当前会话: ${resolved ?? '尚未评估'}`,
154
+ '',
155
+ '说明: ' +
156
+ (configTtl === 'auto'
157
+ ? '自动模式 – 短会话(≤14 轮)用 5m节省写入成本,' +
158
+ '长会话(≥15 轮 或 ≥50k 字符)自动升级到 1h 复用缓存。'
159
+ : `${configTtl} 模式 – TTL 每次请求都固定。`),
160
+ '',
161
+ '/ttl 5m|1h|auto 调整 TTL',
162
+ ];
163
+ return { handled: true, output: lines.join('\n') };
164
+ }
127
165
  // ── /skills [id] ────────────────────────────────────
128
166
  if (name === '/skills') {
129
167
  if (arg) {
@@ -1 +1 @@
1
- {"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatgfsj-code",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Thatgfsj Code - AI Coding Assistant with Reasonix-style prompt caching",
5
5
  "main": "dist/cmd/index.js",
6
6
  "type": "module",
package/src/app/index.ts CHANGED
@@ -38,6 +38,12 @@ export class App {
38
38
  * in the REPL.
39
39
  */
40
40
  showThinking: boolean = false;
41
+ /**
42
+ * v3.0.3: TTL resolved per session. null = not yet decided (auto mode
43
+ * waiting for first round). After the first chatStream completes, this
44
+ * is '5m' or '1h' and stays sticky.
45
+ */
46
+ resolvedTtl: '5m' | '1h' | null = null;
41
47
 
42
48
  private constructor(
43
49
  config: ConfigManager,
@@ -117,6 +123,8 @@ export class App {
117
123
  const msgs = messages || this.session.getMessages();
118
124
  const inner = this.llm.chatStream(msgs);
119
125
  const debugUsage = !!process.env.GFCODE_DEBUG_USAGE;
126
+ // v3.0.3: read TTL the LLMService resolved this round (sticky per session).
127
+ this.resolvedTtl = this.llm.getResolvedTTL();
120
128
  let next = await inner.next();
121
129
  while (!next.done) {
122
130
  // Forward chunks unchanged, but capture usage into the persistent
@@ -12,4 +12,10 @@
12
12
  export { fingerprint, fingerprintTools, fingerprintSystemPrefix } from './fingerprint.js';
13
13
  export { CacheStatsStore, estimateSavingsCNY, type CacheStats, type CacheSnapshot } from './stats.js';
14
14
  export { VolatileScratch } from './volatile.js';
15
- export { shouldDowngrade, type SmartModelDecision } from './smartModel.js';
15
+ export {
16
+ shouldDowngrade,
17
+ decideTTL,
18
+ totalConversationChars,
19
+ type SmartModelDecision,
20
+ type SmartTTLDecision,
21
+ } from './smartModel.js';
@@ -1,24 +1,37 @@
1
1
  /**
2
- * Smart-model routing (P3 of the Reasonix plan).
3
- *
4
- * Idea: classify the user's input as "simple" or "complex" and route
5
- * simple queries to a cheaper / faster model while keeping the main model
6
- * for complex work. The classification is intentionally trivial — prompt
7
- * length + presence of recent tool calls — because the cost of a
8
- * misclassification (a slightly worse answer on a simple query) is much
9
- * lower than the cost of running a frontier model on every greeting.
10
- *
11
- * Trade-off: each routing decision adds one model swap (different
12
- * `model` field on the wire). If the cache prefix is keyed on the model
13
- * string, the cache miss rate can spike on every simple query. To keep
14
- * the prefix stable, callers should pass a *family* (e.g. "anthropic" or
15
- * "deepseek") rather than a specific model name when forwarding the
16
- * prompt to the downstream provider — that's outside the scope of this
17
- * hook; this module only answers the question "should I downgrade?".
18
- *
19
- * v3.0.0: shipped as a stub. Real decision logic is conservative
20
- * (downgrade only when prompt is short AND no recent tool activity).
21
- * Future iterations can add heuristic or classifier-based routing.
2
+ * Smart-model routing + auto TTL selection (P3 of the Reasonix plan,
3
+ * v3.0.3 onwards).
4
+ *
5
+ * Idea: classify the conversation as "short" / "medium" / "long" and
6
+ * route accordingly. Two levers:
7
+ *
8
+ * 1. Model downgrade
9
+ * Short follow-ups ("yes", "thanks") mini model
10
+ * Tool-heavy turns → main model
11
+ *
12
+ * 2. Cache TTL
13
+ * Short conversations → 5m (cheap write, expires fast)
14
+ * Long conversations (>15 turns) 1h (cache hits pay back the
15
+ * expensive write each round)
16
+ *
17
+ * Why TTL is decided per-ROUND but CHANGES per-CONVERSATION:
18
+ * The Anthropic cache_control marker is part of the request body.
19
+ * Changing the TTL between rounds would push the wire bytes to a
20
+ * different Anthropic cache bucket, blowing the cache on every round
21
+ * and *costing* more in cache_creation_input_tokens than what we save.
22
+ *
23
+ * So we compute the TTL once per conversation (lazily, on the first
24
+ * round) and never change it. Subsequent rounds get the same TTL,
25
+ * which keeps the cache prefix stable and lets Anthropic match it.
26
+ *
27
+ * The "smart" part is that we look at the FULL conversation so far,
28
+ * not just the last input. A 16-turn conversation that started with a
29
+ * short prompt but grew into a long refactor still gets 1h.
30
+ *
31
+ * Trade-off: the first round is evaluated with 0 history, so it
32
+ * defaults to 5m. If the conversation then grows long, future rounds
33
+ * still use 5m for the system-token cache (because we already wrote
34
+ * it with 5m). The tool definition cache gets the same TTL.
22
35
  */
23
36
 
24
37
  import type { ChatMessage } from '../types.js';
@@ -30,6 +43,13 @@ export interface SmartModelDecision {
30
43
  reason: string;
31
44
  }
32
45
 
46
+ export interface SmartTTLDecision {
47
+ ttl: '5m' | '1h';
48
+ reason: string;
49
+ /** Computed per-conversation. Once we pick a TTL, we keep it. */
50
+ isFirstDecision: boolean;
51
+ }
52
+
33
53
  /**
34
54
  * Heuristic: downgrade when
35
55
  * 1. The latest user message is short (< 200 chars), AND
@@ -50,3 +70,76 @@ export function shouldDowngrade(messages: ChatMessage[], lastUserInput: string):
50
70
  }
51
71
  return { downgrade: true, reason: 'short-no-tools' };
52
72
  }
73
+
74
+ /**
75
+ * Total character count of all message content. Used as a cheap
76
+ * proxy for "is this conversation long enough to justify 1h TTL?"
77
+ * (Anthropic charges cache_creation at 1.25x normal input, so we
78
+ * need to be sure the conversation will hit cache ~10+ times to
79
+ * break even on 1h vs 5m.)
80
+ */
81
+ export function totalConversationChars(messages: ChatMessage[]): number {
82
+ let total = 0;
83
+ for (const m of messages) {
84
+ if (typeof m.content === 'string') {
85
+ total += m.content.length;
86
+ } else if (Array.isArray(m.content)) {
87
+ for (const blk of m.content) {
88
+ if (blk.type === 'text') total += (blk as any).text.length;
89
+ }
90
+ }
91
+ }
92
+ return total;
93
+ }
94
+
95
+ /**
96
+ * Decide the cache TTL for the current round.
97
+ *
98
+ * Inputs:
99
+ * - messages: full conversation so far (system + user + assistant + tool)
100
+ * - previousTTL: TTL chosen in a prior round (null on round 0)
101
+ *
102
+ * Decision matrix:
103
+ * - round 0 + any input length → 5m (default; user can override via init)
104
+ * - round 1-14 + total chars < 50k → 5m (short sessions, 5m is enough)
105
+ * - round 15+ OR total chars > 50k → 1h (long sessions, 1h amortizes the write)
106
+ *
107
+ * Stability rule: once a TTL is chosen, it is reused for every
108
+ * subsequent round in the same session. Changing TTL between rounds
109
+ * would invalidate the Anthropic cache prefix and cost more than it
110
+ * saves.
111
+ *
112
+ * The `isFirstDecision` flag tells the caller whether to apply the
113
+ * decision (first round) or reuse the previous one (later rounds).
114
+ */
115
+ export function decideTTL(
116
+ messages: ChatMessage[],
117
+ previousTTL: '5m' | '1h' | null,
118
+ ): SmartTTLDecision {
119
+ if (previousTTL) {
120
+ return { ttl: previousTTL, reason: 'reused-from-previous-round', isFirstDecision: false };
121
+ }
122
+
123
+ const turnCount = messages.filter(m => m.role === 'user' || m.role === 'assistant').length;
124
+ const totalChars = totalConversationChars(messages);
125
+
126
+ // Round 0: empty / first input. Default to 5m.
127
+ if (turnCount === 0) {
128
+ return { ttl: '5m', reason: 'first-round-default', isFirstDecision: true };
129
+ }
130
+
131
+ // Multi-turn but small conversation → 5m
132
+ if (turnCount < 15 && totalChars < 50_000) {
133
+ return { ttl: '5m', reason: `short-session-${turnCount}-turns`, isFirstDecision: true };
134
+ }
135
+
136
+ // Long conversations → 1h
137
+ if (turnCount >= 15) {
138
+ return { ttl: '1h', reason: `long-session-${turnCount}-turns`, isFirstDecision: true };
139
+ }
140
+ if (totalChars >= 50_000) {
141
+ return { ttl: '1h', reason: `long-context-${totalChars}-chars`, isFirstDecision: true };
142
+ }
143
+
144
+ return { ttl: '5m', reason: 'fallback', isFirstDecision: true };
145
+ }
@@ -16,12 +16,14 @@ const DEFAULT_CONFIG: Config = {
16
16
  maxTokens: 4096,
17
17
  contextLength: 50,
18
18
  provider: 'siliconflow',
19
- // v3.0.0: prompt cache policy. Default = enabled, 5m TTL, auto strategy
20
- // (Anthropic gets explicit cache_control markers; everything else falls
21
- // through to the provider's built-in caching).
19
+ // v3.0.0: prompt cache policy. Default = enabled, 'auto' TTL.
20
+ // v3.0.3: 'auto' TTL lets the runtime's decideTTL() pick 5m for short
21
+ // sessions and 1h for long ones (>= 15 turns or >= 50k chars). The
22
+ // chosen TTL is sticky across rounds within a session so the cache
23
+ // prefix stays stable.
22
24
  cache: {
23
25
  enabled: true,
24
- ttl: '5m',
26
+ ttl: 'auto',
25
27
  strategy: 'auto',
26
28
  },
27
29
  };
@@ -43,10 +43,14 @@ export interface Config {
43
43
  * Note: enabling/disabling only affects Anthropic; other providers do
44
44
  * not expose a programmatic cache toggle, so the flag is purely
45
45
  * advisory there.
46
+ *
47
+ * v3.0.3: `ttl` accepts '5m' | '1h' | 'auto'. 'auto' lets the runtime
48
+ * pick the TTL based on conversation length (decideTTL in smartModel.ts).
49
+ * The wizard's default is 'auto' so users don't have to choose.
46
50
  */
47
51
  cache?: {
48
52
  enabled: boolean;
49
- ttl?: '5m' | '1h';
53
+ ttl?: '5m' | '1h' | 'auto';
50
54
  strategy?: 'auto' | 'manual' | 'off';
51
55
  };
52
56
  }
@@ -46,11 +46,31 @@ const DEFAULT_CACHE_TTL: '5m' | '1h' = '5m';
46
46
  export class AnthropicProvider implements LLMProvider {
47
47
  readonly name = 'anthropic';
48
48
  protected config: ProviderConfig;
49
+ /**
50
+ * v3.0.3: TTL resolved from the user's config + smart routing. This is
51
+ * the *actual* TTL attached to every cache_control marker we send this
52
+ * session. Stays stable across rounds to keep the Anthropic cache
53
+ * prefix intact.
54
+ *
55
+ * Computed once by LLMService.chatStream when ttl='auto', then
56
+ * persisted via setResolvedTTL. Subsequent rounds call chatStream
57
+ * without re-deciding (ChatOptions.resolvedTtl is sticky per session).
58
+ */
59
+ protected resolvedTtl: '5m' | '1h' = DEFAULT_CACHE_TTL;
49
60
 
50
61
  constructor(config: ProviderConfig) {
51
62
  this.config = config;
52
63
  }
53
64
 
65
+ /**
66
+ * Called by LLMService after decideTTL(). Persists the TTL on the
67
+ * provider instance so buildRequest() writes it consistently across
68
+ * every round in this session.
69
+ */
70
+ setResolvedTTL(ttl: '5m' | '1h'): void {
71
+ this.resolvedTtl = ttl;
72
+ }
73
+
54
74
  buildTools(tools: Tool[]): any[] {
55
75
  return tools.map(tool => ({
56
76
  name: tool.name,
@@ -247,7 +267,12 @@ export class AnthropicProvider implements LLMProvider {
247
267
  // final block (and only the final block, per Anthropic convention)
248
268
  // carries the cache_control marker.
249
269
  const systemMessages = messages.filter(m => m.role === 'system');
250
- const cacheTtl = (this.config.cache?.ttl) || DEFAULT_CACHE_TTL;
270
+ // v3.0.3: TTL is sticky per session. The provider's resolvedTtl is
271
+ // set once by LLMService after decideTTL() and never changed within
272
+ // a session (changing it would invalidate the Anthropic cache
273
+ // prefix and cost more in cache_creation_input_tokens than it
274
+ // saves).
275
+ const cacheTtl = this.resolvedTtl;
251
276
  const cacheEnabled = this.config.cache?.enabled !== false; // default on for Anthropic
252
277
 
253
278
  const systemBlocks = systemMessages.length === 0 ? undefined : systemMessages.map((m, i, arr) => {
package/src/llm/index.ts CHANGED
@@ -20,6 +20,7 @@ import { PROVIDERS } from '../config/providers.js';
20
20
  import { OpenAIProvider } from './openai.js';
21
21
  import { AnthropicProvider } from './anthropic.js';
22
22
  import { GeminiProvider } from './gemini.js';
23
+ import { decideTTL } from '../cache/smartModel.js';
23
24
 
24
25
  export class LLMService {
25
26
  private provider: LLMProvider;
@@ -31,6 +32,19 @@ export class LLMService {
31
32
  this.apiKey = apiKey;
32
33
  }
33
34
 
35
+ /**
36
+ * v3.0.3: TTL resolved from config + smart routing. Stays null until
37
+ * the first round, then never changes for the session. Anthropic
38
+ * provider's setResolvedTTL is called at the same time, so the wire
39
+ * prefix is consistent across rounds.
40
+ */
41
+ private resolvedTtl: '5m' | '1h' | null = null;
42
+
43
+ /** Public accessor used by App.streamResponse to surface TTL in the UI. */
44
+ getResolvedTTL(): '5m' | '1h' | null {
45
+ return this.resolvedTtl;
46
+ }
47
+
34
48
  static fromConfig(config: AIConfig & { cache?: Config['cache'] }): LLMService {
35
49
  const providerName = config.provider || 'siliconflow';
36
50
  const providerConfig = PROVIDERS[providerName];
@@ -43,7 +57,9 @@ export class LLMService {
43
57
  maxTokens: config.maxTokens ?? 4096,
44
58
  // v3.0.0: forward cache policy. Anthropic reads this to decide
45
59
  // whether to attach cache_control markers; other providers ignore it.
46
- cache: config.cache ?? { enabled: true, ttl: '5m', strategy: 'auto' as const },
60
+ // v3.0.3: ttl accepts '5m' | '1h' | 'auto'. 'auto' is resolved
61
+ // per-session by decideTTL() inside chatStream.
62
+ cache: config.cache ?? { enabled: true, ttl: 'auto' as const, strategy: 'auto' as const },
47
63
  };
48
64
 
49
65
  const format = providerConfig.format;
@@ -98,6 +114,27 @@ export class LLMService {
98
114
  ): AsyncGenerator<StreamChunk, ChatResponse> {
99
115
  if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
100
116
 
117
+ // v3.0.3: Resolve TTL once per session. If config says 'auto', call
118
+ // decideTTL to pick 5m (short sessions) or 1h (long ones). For
119
+ // non-Anthropic providers we don't need to do anything — cache
120
+ // markers are wired into the system regardless.
121
+ const configTtl = (this.provider as any).config?.cache?.ttl;
122
+ if (configTtl === 'auto' && this.resolvedTtl === null) {
123
+ const decision = decideTTL(messages, null);
124
+ this.resolvedTtl = decision.ttl;
125
+ if (typeof (this.provider as any).setResolvedTTL === 'function') {
126
+ (this.provider as any).setResolvedTTL(decision.ttl);
127
+ }
128
+ } else if (configTtl === '5m' || configTtl === '1h') {
129
+ // User pinned a specific TTL — apply it once and keep it.
130
+ if (this.resolvedTtl === null) {
131
+ this.resolvedTtl = configTtl;
132
+ if (typeof (this.provider as any).setResolvedTTL === 'function') {
133
+ (this.provider as any).setResolvedTTL(configTtl);
134
+ }
135
+ }
136
+ }
137
+
101
138
  const maxIterations = options?.maxIterations ?? 10;
102
139
  let currentMessages = [...messages];
103
140
  let iterations = 0;
@@ -52,10 +52,15 @@ export interface ProviderConfig {
52
52
  * Optional cache control policy. When omitted, providers fall back to their
53
53
  * default behavior (Anthropic: explicit cache_control markers; OpenAI / Gemini
54
54
  * / DeepSeek: automatic prefix cache, no markers needed).
55
+ *
56
+ * v3.0.3: `ttl` accepts '5m' | '1h' | 'auto'. 'auto' lets the runtime
57
+ * decide per-session via decideTTL() in cache/smartModel.ts. The actual
58
+ * value used at request time is what gets stored on the provider's
59
+ * resolvedTtl field (set by LLMService.chatStream).
55
60
  */
56
61
  cache?: {
57
62
  enabled: boolean;
58
- ttl?: '5m' | '1h';
63
+ ttl?: '5m' | '1h' | 'auto';
59
64
  /**
60
65
  * 'auto' — provider default (Anthropic: explicit, OpenAI/DeepSeek: auto)
61
66
  * 'manual' — always emit cache_control markers regardless of provider
package/src/tui/app.tsx CHANGED
@@ -12,7 +12,6 @@ import { useChat } from './hooks/useChat.js';
12
12
  import { useCommands } from './hooks/useCommands.js';
13
13
  import type { App } from '../app/index.js';
14
14
  import type { MessageData } from './components/ChatMessage.js';
15
- import type { ProviderName } from '../config/types.js';
16
15
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
17
16
  import { join } from 'path';
18
17
  import { homedir } from 'os';
@@ -35,15 +34,13 @@ function saveModelToHistory(model: string) {
35
34
  }
36
35
  }
37
36
 
38
- type ViewMode = 'chat' | 'model_select' | 'init_wizard' | 'init_key' | 'init_url' | 'init_custom_model';
37
+ type ViewMode = 'chat' | 'model_select' | 'init_wizard';
39
38
 
40
39
  export function TuiApp({ app }: Props) {
41
40
  const { messages, isThinking, streaming, streamingToolCalls, queuedMessage, sendMessage, cancel } = useChat(app);
42
41
  const { handleCommand } = useCommands(app);
43
42
  const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
44
43
  const [viewMode, setViewMode] = useState<ViewMode>('chat');
45
- const [initProvider, setInitProvider] = useState<ProviderName>('siliconflow');
46
- const [initUrl, setInitUrl] = useState('');
47
44
  const { stdout } = useStdout();
48
45
  const terminalWidth = stdout?.columns || 80;
49
46
 
@@ -51,8 +48,14 @@ export function TuiApp({ app }: Props) {
51
48
  // change (i.e. a round just completed and recorded new usage). Polling on
52
49
  // an interval would be wasteful; React re-render is the trigger.
53
50
  const [cacheSnapshot, setCacheSnapshot] = useState(() => app.cacheStats.snapshot());
51
+ // v3.0.3: resolved TTL (sticky per session). null until first round.
52
+ const [resolvedTtl, setResolvedTtl] = useState<'5m' | '1h' | null>(app.resolvedTtl);
53
+ // The user-pinned config TTL ('auto' | '5m' | '1h') — used to render the
54
+ // Header chip BEFORE the first round, when resolvedTtl is still null.
55
+ const configTtl = (app.config.get() as any).cache?.ttl as 'auto' | '5m' | '1h' | undefined;
54
56
  useEffect(() => {
55
57
  setCacheSnapshot(app.cacheStats.snapshot());
58
+ setResolvedTtl(app.resolvedTtl);
56
59
  }, [messages.length]);
57
60
 
58
61
  const addMsg = useCallback((content: string) => {
@@ -60,41 +63,6 @@ export function TuiApp({ app }: Props) {
60
63
  }, []);
61
64
 
62
65
  const onSubmit = useCallback(async (input: string) => {
63
- // Init wizard - API Key input
64
- if (viewMode === 'init_key') {
65
- const key = input.trim();
66
- if (!key) { setViewMode('chat'); return; }
67
- // Save and complete
68
- const { App: AppClass } = await import('../app/index.js');
69
- const cfg = app.config.get();
70
- app.config.save({ provider: initProvider, apiKey: key, model: cfg.model || 'default' });
71
- saveModelToHistory(cfg.model || 'default');
72
- setViewMode('chat');
73
- addMsg(`配置已更新: ${initProvider}`);
74
- return;
75
- }
76
-
77
- // Init wizard - Custom URL input
78
- if (viewMode === 'init_url') {
79
- const url = input.trim();
80
- if (!url) { setViewMode('chat'); return; }
81
- setInitUrl(url);
82
- setViewMode('init_key');
83
- return;
84
- }
85
-
86
- // Init wizard - Custom model name
87
- if (viewMode === 'init_custom_model') {
88
- const model = input.trim();
89
- if (model) {
90
- app.config.save({ model });
91
- saveModelToHistory(model);
92
- addMsg(`模型已切换: ${model}`);
93
- }
94
- setViewMode('chat');
95
- return;
96
- }
97
-
98
66
  // Model selector - ignore text input
99
67
  if (viewMode === 'model_select') return;
100
68
 
@@ -125,7 +93,7 @@ export function TuiApp({ app }: Props) {
125
93
  }
126
94
 
127
95
  sendMessage(input);
128
- }, [handleCommand, sendMessage, app, viewMode, initProvider, initUrl, addMsg]);
96
+ }, [handleCommand, sendMessage, app, viewMode, addMsg]);
129
97
 
130
98
  const allMessages = [...systemMessages, ...messages];
131
99
  const activeSkills = app.skills.listActive().map(s => s.id);
@@ -137,6 +105,7 @@ export function TuiApp({ app }: Props) {
137
105
  model={app.config.get().model}
138
106
  cacheHitRate={cacheSnapshot.hitRate > 0 ? cacheSnapshot.hitRate : null}
139
107
  cacheSavingsCNY={cacheSnapshot.estimatedSavingsCNY}
108
+ cacheTtl={resolvedTtl ?? configTtl ?? null}
140
109
  />
141
110
  <ChatList
142
111
  messages={allMessages}
@@ -164,6 +133,12 @@ export function TuiApp({ app }: Props) {
164
133
  onAddNew={() => setViewMode('init_wizard')}
165
134
  />
166
135
  ) : viewMode === 'init_wizard' ? (
136
+ // v3.0.1: InitWizard is now fully self-contained — it owns its own
137
+ // input handling via useInput + ink-text-input. No external UserInput
138
+ // is rendered here, and app.tsx no longer has the text-entry
139
+ // viewModes (init_key / init_url / init_custom_model) because they
140
+ // were the broken input-handling path that left users stuck on a
141
+ // frozen "输入 API Key" screen with no way to type.
167
142
  <InitWizard
168
143
  onComplete={(provider, model, apiKey, baseUrl) => {
169
144
  app.config.save({ provider, model, apiKey, baseUrl });
@@ -175,22 +150,6 @@ export function TuiApp({ app }: Props) {
175
150
  />
176
151
  ) : (
177
152
  <Box flexDirection="column">
178
- {viewMode === 'init_key' && (
179
- <Box paddingLeft={1} marginBottom={0}>
180
- <Text color="#06B6D4" bold>输入 API Key: </Text>
181
- <Text dimColor>({initProvider})</Text>
182
- </Box>
183
- )}
184
- {viewMode === 'init_url' && (
185
- <Box paddingLeft={1} marginBottom={0}>
186
- <Text color="#06B6D4" bold>输入中转站 URL: </Text>
187
- </Box>
188
- )}
189
- {viewMode === 'init_custom_model' && (
190
- <Box paddingLeft={1} marginBottom={0}>
191
- <Text color="#06B6D4" bold>输入模型名称: </Text>
192
- </Box>
193
- )}
194
153
  <UserInput onSubmit={onSubmit} onCancel={cancel} disabled={false} />
195
154
  </Box>
196
155
  )}
@@ -13,6 +13,13 @@ interface Props {
13
13
  */
14
14
  cacheHitRate?: number | null;
15
15
  cacheSavingsCNY?: number;
16
+ /**
17
+ * v3.0.3: cache TTL marker. '5m' (default for short sessions) or '1h'
18
+ * (auto-decided for long sessions, or pinned by the user). 'auto' is
19
+ * the user-pinned setting and shows up as ⏱ auto. null = not yet
20
+ * decided (waiting for first round).
21
+ */
22
+ cacheTtl?: '5m' | '1h' | 'auto' | null;
16
23
  }
17
24
 
18
25
  /**
@@ -27,7 +34,7 @@ function hitRateColor(rate: number): string {
27
34
  return '#6B7280';
28
35
  }
29
36
 
30
- export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY }: Props) {
37
+ export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY, cacheTtl }: Props) {
31
38
  const showCache = typeof cacheHitRate === 'number' && cacheHitRate > 0;
32
39
  return (
33
40
  <Box flexDirection="column" marginBottom={0}>
@@ -35,7 +42,7 @@ export const Header = React.memo(function Header({ provider, model, cacheHitRate
35
42
  <Box>
36
43
  <Text color="#06B6D4" bold> ⚡ </Text>
37
44
  <Text color="#22D3EE" bold>THATGFSJ CODE</Text>
38
- <Text dimColor> v3.0.0</Text>
45
+ <Text dimColor> v3.0.3</Text>
39
46
  </Box>
40
47
  <Box>
41
48
  {showCache && (
@@ -45,6 +52,9 @@ export const Header = React.memo(function Header({ provider, model, cacheHitRate
45
52
  <Text dimColor> · </Text>
46
53
  </>
47
54
  )}
55
+ {cacheTtl && (
56
+ <Text color="#A78BFA" bold> ⏱ {cacheTtl} </Text>
57
+ )}
48
58
  <Text color="#06B6D4" bold> {provider} </Text>
49
59
  <Text dimColor>/</Text>
50
60
  <Text color="#22D3EE"> {model} </Text>