ccoding 0.4.0__tar.gz → 0.5.0__tar.gz

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 (33) hide show
  1. {ccoding-0.4.0 → ccoding-0.5.0}/PKG-INFO +1 -1
  2. {ccoding-0.4.0 → ccoding-0.5.0}/pyproject.toml +1 -1
  3. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/api.py +9 -1
  4. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/config.py +33 -9
  5. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/ui.py +51 -26
  6. {ccoding-0.4.0 → ccoding-0.5.0}/uv.lock +1 -1
  7. {ccoding-0.4.0 → ccoding-0.5.0}/.github/workflows/publish.yml +0 -0
  8. {ccoding-0.4.0 → ccoding-0.5.0}/.gitignore +0 -0
  9. {ccoding-0.4.0 → ccoding-0.5.0}/CLAUDE.md +0 -0
  10. {ccoding-0.4.0 → ccoding-0.5.0}/LICENSE +0 -0
  11. {ccoding-0.4.0 → ccoding-0.5.0}/README.md +0 -0
  12. {ccoding-0.4.0 → ccoding-0.5.0}/screenshot.png +0 -0
  13. {ccoding-0.4.0 → ccoding-0.5.0}/screenshot_mobile1.jpeg +0 -0
  14. {ccoding-0.4.0 → ccoding-0.5.0}/screenshot_mobile2.jpeg +0 -0
  15. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/__init__.py +0 -0
  16. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/__main__.py +0 -0
  17. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/cli.py +0 -0
  18. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/env_schema.json +0 -0
  19. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/logo.py +0 -0
  20. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote.py +0 -0
  21. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_cli.py +0 -0
  22. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_client.py +0 -0
  23. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_common.py +0 -0
  24. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_server.py +0 -0
  25. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/__init__.py +0 -0
  26. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/app.js +0 -0
  27. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/addon-fit.js +0 -0
  28. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm-LICENSE +0 -0
  29. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm-addon-fit-LICENSE +0 -0
  30. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm.css +0 -0
  31. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm.js +0 -0
  32. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/index.html +0 -0
  33. {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/style.css +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ccoding
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Curses-based Claude Code launcher
5
5
  Project-URL: Homepage, https://github.com/8DE4732A/ccode
6
6
  Project-URL: Repository, https://github.com/8DE4732A/ccode
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ccoding"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Curses-based Claude Code launcher"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -9,6 +9,9 @@ from typing import Any
9
9
 
10
10
  from .config import MODEL_ENV_VARS, MODEL_KEYS, REQUIRED_MODEL_KEYS, mask_secret
11
11
 
12
+ # Claude Code 识别小写的 1M 上下文变体后缀(如 claude-sonnet-4-5[1m])
13
+ MODEL_1M_SUFFIX = "[1m]"
14
+
12
15
 
13
16
  def _truncate_body(text: str, max_len: int = 200) -> str:
14
17
  return f"{text[:max_len]}..." if len(text) > max_len else text
@@ -69,7 +72,9 @@ def validate_models(config: dict[str, Any], models: list[dict[str, str]]) -> boo
69
72
  model_id = entry.get("id")
70
73
  if not owned_by or not model_id or (owned_by, model_id) not in valid:
71
74
  if entry.get("owned_by") is not None or entry.get("id") is not None:
72
- config["models"][key] = {"owned_by": None, "id": None}
75
+ # 只清空选择字段,保留 1m 等模型级偏好和未来新增字段
76
+ entry["owned_by"] = None
77
+ entry["id"] = None
73
78
  changed = True
74
79
  return changed
75
80
 
@@ -107,6 +112,9 @@ def build_env(config: dict[str, Any], masked: bool = False) -> dict[str, str]:
107
112
  entry = config["models"].get(key, {})
108
113
  model_id = entry.get("id")
109
114
  if model_id:
115
+ # 用户开启 1M 开关时追加官方小写后缀;模型 id 本身已带后缀则不重复追加。
116
+ if entry.get("1m") and not model_id.lower().endswith(MODEL_1M_SUFFIX):
117
+ model_id = f"{model_id}{MODEL_1M_SUFFIX}"
110
118
  env[MODEL_ENV_VARS[key]] = model_id
111
119
  for key, value in config.get("toggles", {}).items():
112
120
  env[key] = str(value)
@@ -89,11 +89,20 @@ def env_schema_enum(key: str) -> list[str]:
89
89
  return load_env_schema().get(key, {}).get("enum", [])
90
90
 
91
91
 
92
+ def model_entry(
93
+ owner: str | None = None,
94
+ model_id: str | None = None,
95
+ one_m: bool = False,
96
+ ) -> dict[str, Any]:
97
+ """模型条目的唯一构造入口,新增字段时只需改这里。"""
98
+ return {"owned_by": owner, "id": model_id, "1m": one_m}
99
+
100
+
92
101
  def default_config() -> dict[str, Any]:
93
102
  return {
94
103
  "base_url": DEFAULT_BASE_URL,
95
104
  "api_key": "",
96
- "models": {key: {"owned_by": None, "id": None} for key in MODEL_KEYS},
105
+ "models": {key: model_entry() for key in MODEL_KEYS},
97
106
  "toggles": DEFAULT_TOGGLES.copy(),
98
107
  "skip_permissions": False,
99
108
  "remote": copy.deepcopy(DEFAULT_REMOTE),
@@ -208,10 +217,12 @@ def load_config() -> dict[str, Any]:
208
217
  if isinstance(entry, dict):
209
218
  owned_by = entry.get("owned_by")
210
219
  model_id = entry.get("id")
211
- config["models"][key]["owned_by"] = (
212
- owned_by if isinstance(owned_by, str) else None
220
+ one_m = entry.get("1m")
221
+ config["models"][key] = model_entry(
222
+ owned_by if isinstance(owned_by, str) else None,
223
+ model_id if isinstance(model_id, str) else None,
224
+ one_m if isinstance(one_m, bool) else False,
213
225
  )
214
- config["models"][key]["id"] = model_id if isinstance(model_id, str) else None
215
226
 
216
227
  toggles = data.get("toggles")
217
228
  if isinstance(toggles, dict):
@@ -251,16 +262,29 @@ def mask_secret(value: str) -> str:
251
262
  return f"{value[:4]}...{value[-4:]}"
252
263
 
253
264
 
265
+ def _model_entry(
266
+ config: dict[str, Any],
267
+ key: str,
268
+ owner: str | None,
269
+ model_id: str | None,
270
+ ) -> dict[str, Any]:
271
+ """重建模型条目,保留已有的 1M 上下文开关状态。"""
272
+ existing = config["models"].get(key, {})
273
+ return model_entry(owner if owner else None, model_id, bool(existing.get("1m")))
274
+
275
+
254
276
  def update_model_owner(config: dict[str, Any], key: str, owner: str | None) -> None:
255
- config["models"][key] = {"owned_by": owner if owner else None, "id": None}
277
+ config["models"][key] = _model_entry(config, key, owner, None)
256
278
  save_config(config)
257
279
 
258
280
 
259
281
  def update_model_id(
260
282
  config: dict[str, Any], key: str, owner: str | None, model_id: str | None
261
283
  ) -> None:
262
- if owner and model_id:
263
- config["models"][key] = {"owned_by": owner, "id": model_id}
264
- else:
265
- config["models"][key] = {"owned_by": owner if owner else None, "id": None}
284
+ config["models"][key] = _model_entry(config, key, owner, model_id)
285
+ save_config(config)
286
+
287
+
288
+ def update_model_1m(config: dict[str, Any], key: str, enabled: bool) -> None:
289
+ config["models"][key]["1m"] = bool(enabled)
266
290
  save_config(config)
@@ -23,6 +23,7 @@ from .config import (
23
23
  load_config,
24
24
  mask_secret,
25
25
  save_config,
26
+ update_model_1m,
26
27
  update_model_id,
27
28
  update_model_owner,
28
29
  )
@@ -56,6 +57,16 @@ _FIELD_REMOTE_REUSE_SESSION = "remote.reuse_session"
56
57
  _FIELD_TOGGLE = "toggle:" # 前缀 + key
57
58
  _FIELD_ADD_TOGGLE = "__add__" # 新增行
58
59
 
60
+ # 主界面可聚焦字段索引,渲染与按键逻辑共用
61
+ _MAIN_FIELD_OWNER = 0
62
+ _MAIN_FIELD_MODEL = 1
63
+ _MAIN_FIELD_ONE_M = 2
64
+ _MAIN_FIELD_COUNT = 3
65
+
66
+ # 主界面 1M 上下文开关的显示文本(实际注入环境变量时用官方小写 [1m])
67
+ _ONE_M_LABEL = "[1M]"
68
+ _ONE_M_GAP = " "
69
+
59
70
 
60
71
  def addstr_safe(stdscr: curses.window, y: int, x: int, text: str, attr: int = 0) -> None:
61
72
  height, width = stdscr.getmaxyx()
@@ -213,7 +224,7 @@ class CursesApp:
213
224
  pair_id += 1
214
225
 
215
226
  def render_logo(self, stdscr: curses.window, start_y: int) -> None:
216
- kwargs = dict(
227
+ kwargs: dict[str, Any] = dict(
217
228
  stdscr=stdscr,
218
229
  lines=self.logo_lines,
219
230
  start_y=start_y,
@@ -221,9 +232,7 @@ class CursesApp:
221
232
  use_color=self.use_color,
222
233
  color_pairs=self.color_pairs,
223
234
  )
224
- if self.render_style == "wave":
225
- render_style_wave(**kwargs)
226
- elif self.render_style == "pulse":
235
+ if self.render_style == "pulse":
227
236
  render_style_pulse(**kwargs)
228
237
  elif self.render_style == "glitch":
229
238
  render_style_glitch(**kwargs)
@@ -262,6 +271,12 @@ class CursesApp:
262
271
  # ------------------------------------------------------------------
263
272
  # Main screen
264
273
  # ------------------------------------------------------------------
274
+ def _cell_attr(self, row: int, field: int) -> int:
275
+ """返回主界面单元格的显示属性:聚焦时反色。"""
276
+ if row == self.main_focus_row and field == self.main_focus_field:
277
+ return curses.A_REVERSE
278
+ return 0
279
+
265
280
  def render_main(self, stdscr: curses.window) -> None:
266
281
  try:
267
282
  curses.curs_set(0)
@@ -272,15 +287,17 @@ class CursesApp:
272
287
  self.render_logo(stdscr, start_y)
273
288
  y = start_y + len(self.logo_lines) + 1
274
289
  _height, width = stdscr.getmaxyx()
275
- rows: list[tuple[str, str, str, int]] = []
290
+ rows: list[tuple[str, str, str, str]] = []
276
291
  max_row = 0
277
292
  for key in MODEL_KEYS:
293
+ entry = self.config["models"].get(key, {})
278
294
  label = f"{MODEL_LABELS[key]}:".ljust(8)
279
- owner = self.config["models"].get(key, {}).get("owned_by") or "owned_by"
280
- model_id = self.config["models"].get(key, {}).get("id") or "model_id"
281
- row_width = len(label) + 1 + len(owner) + len(" | ") + len(model_id)
295
+ owner = entry.get("owned_by") or "owned_by"
296
+ model_id = entry.get("id") or "model_id"
297
+ one_m = f"{_ONE_M_LABEL} {'on' if entry.get('1m') else 'off'}"
298
+ row_width = len(f"{label} {owner} | {model_id}{_ONE_M_GAP}{one_m}")
282
299
  max_row = max(max_row, row_width)
283
- rows.append((label, owner, model_id, row_width))
300
+ rows.append((label, owner, model_id, one_m))
284
301
  remote = self.config.get("remote", {})
285
302
  mode = remote_mode(self.config)
286
303
  if self.remote_enabled:
@@ -294,28 +311,20 @@ class CursesApp:
294
311
  host, _port = remote_local_host_port(self.config)
295
312
  if mode == "local" and host == "0.0.0.0":
296
313
  remote_line += " [warning: network exposed]"
297
- hint = "enter to start, r remote on/off, c config, b refresh, a/d change model, q quit"
314
+ hint = "enter to start, c config, b refresh, a/d change, ←/→ field, r remote, q quit"
298
315
  content_width = max(max_row, len(remote_line), len(hint), len(self.status_message))
299
316
  x = max(2, (width - content_width) // 2)
300
- for idx, (label, owner, model_id, _row_width) in enumerate(rows):
317
+ for idx, (label, owner, model_id, one_m) in enumerate(rows):
301
318
  addstr_safe(stdscr, y, x, label)
302
319
  owner_x = x + len(label) + 1
303
- owner_attr = (
304
- curses.A_REVERSE
305
- if (idx == self.main_focus_row and self.main_focus_field == 0)
306
- else 0
307
- )
308
- addstr_safe(stdscr, y, owner_x, owner, owner_attr)
320
+ addstr_safe(stdscr, y, owner_x, owner, self._cell_attr(idx, _MAIN_FIELD_OWNER))
309
321
  sep = " | "
310
322
  sep_x = owner_x + len(owner)
311
323
  addstr_safe(stdscr, y, sep_x, sep)
312
324
  model_x = sep_x + len(sep)
313
- model_attr = (
314
- curses.A_REVERSE
315
- if (idx == self.main_focus_row and self.main_focus_field == 1)
316
- else 0
317
- )
318
- addstr_safe(stdscr, y, model_x, model_id, model_attr)
325
+ addstr_safe(stdscr, y, model_x, model_id, self._cell_attr(idx, _MAIN_FIELD_MODEL))
326
+ one_m_x = model_x + len(model_id) + len(_ONE_M_GAP)
327
+ addstr_safe(stdscr, y, one_m_x, one_m, self._cell_attr(idx, _MAIN_FIELD_ONE_M))
319
328
  y += 1
320
329
  y += 1
321
330
  addstr_safe(stdscr, y, x, remote_line)
@@ -859,10 +868,12 @@ class CursesApp:
859
868
  self.main_focus_row = min(len(MODEL_KEYS) - 1, self.main_focus_row + 1)
860
869
  return
861
870
  if key == curses.KEY_LEFT:
862
- self.main_focus_field = 0
871
+ self.main_focus_field = max(0, self.main_focus_field - 1)
863
872
  return
864
873
  if key == curses.KEY_RIGHT:
865
- self.main_focus_field = 1
874
+ self.main_focus_field = min(
875
+ _MAIN_FIELD_COUNT - 1, self.main_focus_field + 1
876
+ )
866
877
  return
867
878
  if key in (ord("["), ord("a")):
868
879
  self.cycle_main_option(-1)
@@ -883,6 +894,12 @@ class CursesApp:
883
894
  if key in (ord("q"), ord("Q")):
884
895
  self.should_exit = True
885
896
  return
897
+ if (
898
+ key in (curses.KEY_ENTER, 10, 13, ord(" "))
899
+ and self.main_focus_field == _MAIN_FIELD_ONE_M
900
+ ):
901
+ self.toggle_main_1m()
902
+ return
886
903
  if key in (curses.KEY_ENTER, 10, 13):
887
904
  self.status_message = ""
888
905
  error = validate_launch_requirements(self.config)
@@ -898,9 +915,17 @@ class CursesApp:
898
915
  else:
899
916
  self.should_exit = True
900
917
 
918
+ def toggle_main_1m(self) -> None:
919
+ key = MODEL_KEYS[self.main_focus_row]
920
+ entry = self.config["models"].get(key, {})
921
+ update_model_1m(self.config, key, not bool(entry.get("1m")))
922
+
901
923
  def cycle_main_option(self, direction: int) -> None:
902
924
  key = MODEL_KEYS[self.main_focus_row]
903
- if self.main_focus_field == 0:
925
+ if self.main_focus_field == _MAIN_FIELD_ONE_M:
926
+ self.toggle_main_1m()
927
+ return
928
+ if self.main_focus_field == _MAIN_FIELD_OWNER:
904
929
  owners = owner_options(self.models_by_owner)
905
930
  if not owners:
906
931
  return
@@ -36,7 +36,7 @@ wheels = [
36
36
 
37
37
  [[package]]
38
38
  name = "ccoding"
39
- version = "0.3.0"
39
+ version = "0.5.0"
40
40
  source = { editable = "." }
41
41
  dependencies = [
42
42
  { name = "windows-curses", marker = "sys_platform == 'win32'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes