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.
- {ccoding-0.4.0 → ccoding-0.5.0}/PKG-INFO +1 -1
- {ccoding-0.4.0 → ccoding-0.5.0}/pyproject.toml +1 -1
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/api.py +9 -1
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/config.py +33 -9
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/ui.py +51 -26
- {ccoding-0.4.0 → ccoding-0.5.0}/uv.lock +1 -1
- {ccoding-0.4.0 → ccoding-0.5.0}/.github/workflows/publish.yml +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/.gitignore +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/CLAUDE.md +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/LICENSE +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/README.md +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/screenshot.png +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/screenshot_mobile1.jpeg +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/screenshot_mobile2.jpeg +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/__init__.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/__main__.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/cli.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/env_schema.json +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/logo.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_cli.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_client.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_common.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/remote_server.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/__init__.py +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/app.js +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/addon-fit.js +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm-LICENSE +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm-addon-fit-LICENSE +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm.css +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/assets/xterm.js +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/index.html +0 -0
- {ccoding-0.4.0 → ccoding-0.5.0}/src/ccode/web/style.css +0 -0
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
212
|
-
|
|
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] =
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 == "
|
|
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,
|
|
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 =
|
|
280
|
-
model_id =
|
|
281
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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 =
|
|
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 ==
|
|
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
|
|
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
|
|
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
|