tagword-codeagent 1.1.6__py3-none-any.whl

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 (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,159 @@
1
+ """Per-use-type default preset IDs.
2
+
3
+ Extends seed's `get_default_preset_id()` (which is a single chat-only default)
4
+ with a multi-slot default map: ``{chat: 'preset-abc', vision: 'preset-xyz', ...}``.
5
+
6
+ Storage:
7
+ - Primary: ``<config_dir>/default_presets.json`` (a JSON object)
8
+ - Legacy fallback: ``<config_dir>/codeagent.default_preset_ids.json``
9
+ - Ultimate fallback: seed's ``seed.default_model`` (chat only)
10
+
11
+ Slots: ``chat``, ``vision``, ``image_gen``, ``audio``, ``music``, ``video_gen``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from pathlib import Path
19
+ from typing import Dict
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Ordered list of supported slots. "chat" must come first for legacy compat.
24
+ SLOTS: tuple[str, ...] = (
25
+ "chat",
26
+ "vision",
27
+ "image_gen",
28
+ "audio",
29
+ "music",
30
+ "video_gen",
31
+ )
32
+
33
+ DEFAULT_PRESET_IDS_FILENAME = "default_presets.json"
34
+ LEGACY_PRESET_IDS_FILENAME = "codeagent.default_preset_ids.json"
35
+
36
+
37
+ def _config_dir() -> Path:
38
+ """Resolve the same config dir seed.core.llm_presets uses."""
39
+ try:
40
+ from seed.core.llm_presets import _config_dir as _seed_config_dir # type: ignore
41
+ return _seed_config_dir()
42
+ except Exception:
43
+ # Fallback (shouldn't normally trigger): SEED_PROJECT_ROOT/config
44
+ import os
45
+ root = os.environ.get("SEED_PROJECT_ROOT", "").strip() or "."
46
+ return Path(root) / "config"
47
+
48
+
49
+ def _new_path() -> Path:
50
+ return _config_dir() / DEFAULT_PRESET_IDS_FILENAME
51
+
52
+
53
+ def _legacy_path() -> Path | None:
54
+ """Path of the legacy single-id file from seed, if any."""
55
+ try:
56
+ from seed.core.llm_presets import _default_id_read_path # type: ignore
57
+ p = _default_id_read_path()
58
+ return p if p is not None else None
59
+ except Exception:
60
+ return None
61
+
62
+
63
+ def _read_legacy_single_id() -> str:
64
+ p = _legacy_path()
65
+ if p is None or not p.is_file():
66
+ return ""
67
+ try:
68
+ return p.read_text(encoding="utf-8").strip()
69
+ except OSError:
70
+ return ""
71
+
72
+
73
+ def get_default_preset_ids() -> Dict[str, str]:
74
+ """Return a mapping ``{slot: preset_id}`` for every slot that has a default.
75
+
76
+ Reads the new JSON map first; falls back to ``codeagent.default_preset_ids.json``
77
+ (legacy), then the seed single-id file (assigned to ``chat``).
78
+ """
79
+ p = _new_path()
80
+ if p.is_file():
81
+ try:
82
+ data = json.loads(p.read_text(encoding="utf-8") or "{}")
83
+ except (json.JSONDecodeError, OSError):
84
+ data = {}
85
+ if isinstance(data, dict):
86
+ out: Dict[str, str] = {}
87
+ for slot in SLOTS:
88
+ v = data.get(slot)
89
+ if isinstance(v, str) and v.strip():
90
+ out[slot] = v.strip()
91
+ return out
92
+ # Legacy fallback 1: old codeagent.default_preset_ids.json
93
+ leg = _config_dir() / LEGACY_PRESET_IDS_FILENAME
94
+ if leg.is_file():
95
+ try:
96
+ data = json.loads(leg.read_text(encoding="utf-8") or "{}")
97
+ except (json.JSONDecodeError, OSError):
98
+ data = {}
99
+ if isinstance(data, dict):
100
+ out: Dict[str, str] = {}
101
+ for slot in SLOTS:
102
+ v = data.get(slot)
103
+ if isinstance(v, str) and v.strip():
104
+ out[slot] = v.strip()
105
+ if out:
106
+ return out
107
+ # Legacy fallback 2: single id assigned to "chat"
108
+ legacy = _read_legacy_single_id()
109
+ if legacy:
110
+ return {"chat": legacy}
111
+ return {}
112
+
113
+
114
+ def set_default_preset_id_for_slot(slot: str, preset_id: str) -> None:
115
+ """Set/clear the default for a single slot. Empty string clears."""
116
+ if slot not in SLOTS:
117
+ raise ValueError(f"unknown slot: {slot!r}")
118
+ cur = get_default_preset_ids()
119
+ pid = (preset_id or "").strip()
120
+ if pid:
121
+ cur[slot] = pid
122
+ else:
123
+ cur.pop(slot, None)
124
+ _write_default_preset_ids(cur)
125
+
126
+
127
+ def set_default_preset_ids(mapping: Dict[str, str]) -> None:
128
+ """Bulk-replace the entire default map (ignoring unknown slots)."""
129
+ out: Dict[str, str] = {}
130
+ for slot in SLOTS:
131
+ if slot in mapping:
132
+ v = mapping[slot]
133
+ if isinstance(v, str) and v.strip():
134
+ out[slot] = v.strip()
135
+ _write_default_preset_ids(out)
136
+
137
+
138
+ def _write_default_preset_ids(mapping: Dict[str, str]) -> None:
139
+ p = _new_path()
140
+ p.parent.mkdir(parents=True, exist_ok=True)
141
+ try:
142
+ p.write_text(json.dumps(mapping, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
143
+ except OSError as e:
144
+ logger.error("Failed to save default preset ids: %s", e)
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Backward-compat shim — keep `get_default_preset_id` and `set_default_preset_id`
149
+ # behaving as before for any in-process callers that still import them.
150
+ # ---------------------------------------------------------------------------
151
+
152
+ def get_default_preset_id() -> str:
153
+ """Legacy single-id shim → returns the 'chat' default (or '' if none)."""
154
+ return get_default_preset_ids().get("chat", "")
155
+
156
+
157
+ def set_default_preset_id(preset_id: str) -> None:
158
+ """Legacy single-id shim → sets the 'chat' default."""
159
+ set_default_preset_id_for_slot("chat", preset_id)
codeagent/core/env.py ADDED
@@ -0,0 +1,106 @@
1
+ """Code Agent product environment variables (``CODEAGENT_*`` only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ # --- Product data root (Code Agent home, not Seed generic ``~/.seed``) ---
9
+
10
+ HOME = "CODEAGENT_HOME"
11
+ PROJECT_ROOT = "CODEAGENT_PROJECT_ROOT"
12
+
13
+ # --- Product keys (not read by Seed) ---
14
+
15
+ AGENT_ID = "CODEAGENT_AGENT_ID"
16
+ LOG_LEVEL = "CODEAGENT_LOG_LEVEL"
17
+ SKILLS_AUTO = "CODEAGENT_SKILLS_AUTO"
18
+ SKILLS_TOP_K = "CODEAGENT_SKILLS_TOP_K"
19
+ DIARY = "CODEAGENT_DIARY"
20
+ DIARY_KEEP_DAYS = "CODEAGENT_DIARY_KEEP_DAYS"
21
+ CHAT_USER_ROUNDS = "CODEAGENT_CHAT_USER_ROUNDS"
22
+ MAX_TOOL_ROUNDS = "CODEAGENT_MAX_TOOL_ROUNDS"
23
+ CHAT_AUTO_CONTINUE_ON_LIMIT = "CODEAGENT_CHAT_AUTO_CONTINUE_ON_LIMIT"
24
+ CHAT_AUTO_CONTINUE_MAX_SEGMENTS = "CODEAGENT_CHAT_AUTO_CONTINUE_MAX_SEGMENTS"
25
+ CHAT_MAX_TOOL_ROUNDS_DEFAULT = "CODEAGENT_CHAT_MAX_TOOL_ROUNDS_DEFAULT"
26
+ CONTEXT_COMPACT = "CODEAGENT_CONTEXT_COMPACT"
27
+ CONTEXT_COMPACT_MIN_TOKENS = "CODEAGENT_CONTEXT_COMPACT_MIN_TOKENS"
28
+ CONTEXT_COMPACT_KEEP_USER_ROUNDS = "CODEAGENT_CONTEXT_COMPACT_KEEP_USER_ROUNDS"
29
+ CONTEXT_COMPACT_MID_LOOP = "CODEAGENT_CONTEXT_COMPACT_MID_LOOP"
30
+ CONTEXT_COMPACT_ADAPTIVE_KEEP = "CODEAGENT_CONTEXT_COMPACT_ADAPTIVE_KEEP"
31
+ CONTEXT_COMPACT_SUMMARIZER_PROMPT_FILE = "CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_PROMPT_FILE"
32
+ CONTEXT_COMPACT_SUMMARIZER_BASEURL = "CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_BASEURL"
33
+ CONTEXT_COMPACT_SUMMARIZER_MODEL = "CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MODEL"
34
+ CONTEXT_COMPACT_SUMMARIZER_MAX_TOKENS = "CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_MAX_TOKENS"
35
+ CHAT_MAX_TOKENS = "CODEAGENT_CHAT_MAX_TOKENS"
36
+ LLM_CONTEXT_SIZE = "CODEAGENT_LLM_CONTEXT_SIZE"
37
+ WEBUI_SESSION_HISTORY_MAX_CHARS = "CODEAGENT_WEBUI_SESSION_HISTORY_MAX_CHARS"
38
+ WEBUI_SESSION_HISTORY_USER_BLOCKS = "CODEAGENT_WEBUI_SESSION_HISTORY_USER_BLOCKS"
39
+ WEBUI_SESSION_HISTORY_MAX_MESSAGES = "CODEAGENT_WEBUI_SESSION_HISTORY_MAX_MESSAGES"
40
+ WEBUI_SESSION_HISTORY_REASONING_MAX_CHARS = "CODEAGENT_WEBUI_SESSION_HISTORY_REASONING_MAX_CHARS"
41
+ WEBUI_TOKEN = "CODEAGENT_WEBUI_TOKEN"
42
+ SKIP_FOLDER_PICKER = "CODEAGENT_SKIP_FOLDER_PICKER"
43
+ AGENT_TOOLS_NO_CACHE = "CODEAGENT_AGENT_TOOLS_NO_CACHE"
44
+ AGENT_TOOLS_MODE = "CODEAGENT_AGENT_TOOLS_MODE"
45
+ ORCHESTRATOR_AUTO_SPLIT = "CODEAGENT_ORCHESTRATOR_AUTO_SPLIT"
46
+
47
+
48
+ def pick_default(default: str, key: str) -> str:
49
+ if key in os.environ:
50
+ return os.environ[key]
51
+ return default
52
+
53
+
54
+ def pick_nonempty(key: str) -> str:
55
+ raw = os.environ.get(key)
56
+ if raw is not None and str(raw).strip():
57
+ return str(raw).strip()
58
+ return ""
59
+
60
+
61
+ def pick_int(default: int, key: str) -> int:
62
+ raw = pick_default(str(default), key).strip()
63
+ try:
64
+ return int(raw)
65
+ except (TypeError, ValueError):
66
+ return default
67
+
68
+
69
+
70
+
71
+ def env_truthy(key: str, default: str = "0") -> bool:
72
+ val = pick_default(default, key).strip().lower()
73
+ return val in ("1", "true", "yes", "on")
74
+
75
+
76
+ def env_falsy(key: str, default: str = "1") -> bool:
77
+ val = pick_default(default, key).strip().lower()
78
+ return val in ("0", "false", "no", "off")
79
+
80
+
81
+ def default_agent_id() -> str:
82
+ return pick_default("default", AGENT_ID).strip() or "default"
83
+
84
+
85
+ def product_home() -> Path:
86
+ """Default ``~/.codeagent``; override with ``CODEAGENT_HOME`` or ``CODEAGENT_PROJECT_ROOT``."""
87
+ raw = pick_nonempty(PROJECT_ROOT) or pick_nonempty(HOME)
88
+ if raw:
89
+ return Path(raw).expanduser().resolve()
90
+ return (Path.home() / ".codeagent").resolve()
91
+
92
+
93
+ def apply_default_product_home() -> Path:
94
+ """
95
+ Ensure product home exists and set ``SEED_PROJECT_ROOT`` for the Seed kernel.
96
+
97
+ Other products (e.g. designagent) should use their own ``~/.<product>`` and set
98
+ ``SEED_PROJECT_ROOT`` the same way in their bootstrap.
99
+ """
100
+ home = product_home()
101
+ os.environ.setdefault(HOME, str(home))
102
+ if "SEED_PROJECT_ROOT" not in os.environ:
103
+ os.environ["SEED_PROJECT_ROOT"] = str(home)
104
+ home.mkdir(parents=True, exist_ok=True)
105
+ (home / "config").mkdir(parents=True, exist_ok=True)
106
+ return home
@@ -0,0 +1,151 @@
1
+ """Native folder picker (macOS / Windows / Linux desktop)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+
12
+ def has_graphical_session() -> bool:
13
+ if sys.platform == "win32":
14
+ return True
15
+ if sys.platform == "darwin":
16
+ return True
17
+ return bool(
18
+ os.environ.get("DISPLAY", "").strip()
19
+ or os.environ.get("WAYLAND_DISPLAY", "").strip()
20
+ )
21
+
22
+
23
+ def pick_directory_sync() -> Tuple[str, bool, str]:
24
+ """
25
+ Blocking folder picker.
26
+
27
+ Returns ``(path, skipped, hint)``:
28
+ - *path*: chosen directory or ``""``
29
+ - *skipped*: env/disabled/no GUI tool
30
+ - *hint*: user-facing reason when *path* is empty
31
+ """
32
+ if sys.platform == "darwin":
33
+ return _pick_darwin()
34
+ if sys.platform == "win32":
35
+ return _pick_windows()
36
+ return _pick_linux()
37
+
38
+
39
+ def _pick_darwin() -> Tuple[str, bool, str]:
40
+ script = (
41
+ 'POSIX path of (choose folder with prompt '
42
+ '"选择目录" default location (path to desktop folder))'
43
+ )
44
+ try:
45
+ cp = subprocess.run(
46
+ ["osascript", "-e", script],
47
+ check=False,
48
+ capture_output=True,
49
+ text=True,
50
+ encoding="utf-8",
51
+ errors="replace",
52
+ timeout=120,
53
+ )
54
+ except FileNotFoundError:
55
+ return "", True, "osascript not found"
56
+ except subprocess.TimeoutExpired:
57
+ return "", False, "folder picker timed out"
58
+ cand = (cp.stdout or "").strip()
59
+ if cand and Path(cand).is_dir():
60
+ return cand, False, ""
61
+ if cp.returncode != 0:
62
+ return "", False, "folder picker cancelled"
63
+ return "", False, ""
64
+
65
+
66
+ def _pick_windows() -> Tuple[str, bool, str]:
67
+ ps = r"""
68
+ Add-Type -AssemblyName System.Windows.Forms
69
+ $top = New-Object System.Windows.Forms.Form
70
+ $top.TopMost = $true
71
+ $top.WindowState = [System.Windows.Forms.FormWindowState]::Minimized
72
+ $top.ShowInTaskbar = $false
73
+ $top.Size = New-Object System.Drawing.Size(0, 0)
74
+ $top.StartPosition = "Manual"
75
+ $top.Location = New-Object System.Drawing.Point(-32000, -32000)
76
+ $null = $top.Show()
77
+ [System.Windows.Forms.Application]::DoEvents()
78
+ try {
79
+ $d = New-Object System.Windows.Forms.FolderBrowserDialog
80
+ $d.Description = "选择项目目录"
81
+ $d.ShowNewFolderButton = $true
82
+ if ($d.ShowDialog($top) -eq "OK") { Write-Output $d.SelectedPath }
83
+ } finally { $top.Close(); $top.Dispose() }
84
+ """
85
+ for exe in ("powershell", "pwsh"):
86
+ if not _which(exe):
87
+ continue
88
+ try:
89
+ cp = subprocess.run(
90
+ [exe, "-NoProfile", "-Command", ps],
91
+ check=False,
92
+ capture_output=True,
93
+ text=True,
94
+ encoding="utf-8",
95
+ errors="replace",
96
+ timeout=120,
97
+ )
98
+ except subprocess.TimeoutExpired:
99
+ return "", False, "folder picker timed out"
100
+ cand = (cp.stdout or "").strip()
101
+ if cand and Path(cand).is_dir():
102
+ return cand, False, ""
103
+ return "", True, "PowerShell not found for folder picker"
104
+
105
+
106
+ def _pick_linux() -> Tuple[str, bool, str]:
107
+ if not has_graphical_session():
108
+ return (
109
+ "",
110
+ True,
111
+ "no DISPLAY/WAYLAND_DISPLAY; set project path manually or install zenity on a desktop session",
112
+ )
113
+
114
+ tools = (
115
+ (["zenity", "--file-selection", "--directory"], "zenity"),
116
+ (["kdialog", "--getexistingdirectory"], "kdialog"),
117
+ (["yad", "--file", "--directory"], "yad"),
118
+ )
119
+ missing: list[str] = []
120
+ for cmd, name in tools:
121
+ if not _which(cmd[0]):
122
+ missing.append(name)
123
+ continue
124
+ try:
125
+ cp = subprocess.run(
126
+ cmd,
127
+ check=False,
128
+ capture_output=True,
129
+ text=True,
130
+ encoding="utf-8",
131
+ errors="replace",
132
+ timeout=120,
133
+ )
134
+ except subprocess.TimeoutExpired:
135
+ return "", False, "folder picker timed out"
136
+ cand = (cp.stdout or "").strip()
137
+ if cand and Path(cand).is_dir():
138
+ return cand, False, ""
139
+ if cp.returncode != 0 and cp.returncode != 1:
140
+ continue
141
+ hint = (
142
+ "install zenity, kdialog, or yad for GUI folder selection "
143
+ f"(missing: {', '.join(missing) or 'unknown'})"
144
+ )
145
+ return "", True, hint
146
+
147
+
148
+ def _which(name: str) -> Optional[str]:
149
+ from shutil import which as _w
150
+
151
+ return _w(name)
@@ -0,0 +1,90 @@
1
+ """Image understanding: vision LLM presets or MiniMax Token Plan MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ from seed.integrations.mcp_config import MINIMAX_MCP_SERVER_ID, get_server_config
9
+
10
+ MCP_VISION_SENTINEL = "__mcp_minimax__"
11
+ MINIMAX_UNDERSTAND_IMAGE_TOOL = "understand_image"
12
+ MCP_QUALIFIED_UNDERSTAND_IMAGE = f"mcp__{MINIMAX_MCP_SERVER_ID}__{MINIMAX_UNDERSTAND_IMAGE_TOOL}"
13
+
14
+
15
+ def minimax_mcp_configured(base: Optional[Path] = None) -> bool:
16
+ """MiniMax MCP server enabled in config with a non-empty API key."""
17
+ from seed.integrations.mcp_client import mcp_globally_enabled
18
+
19
+ if not mcp_globally_enabled():
20
+ return False
21
+ cfg = get_server_config(MINIMAX_MCP_SERVER_ID, base)
22
+ if not cfg or not cfg.enabled or not cfg.command.strip():
23
+ return False
24
+ key = (cfg.env or {}).get("MINIMAX_API_KEY") or ""
25
+ return bool(str(key).strip())
26
+
27
+
28
+ def minimax_mcp_understand_image_ready(
29
+ base: Optional[Path] = None,
30
+ *,
31
+ probe: bool = False,
32
+ servers_status: Optional[list[dict[str, Any]]] = None,
33
+ ) -> bool:
34
+ """True when MiniMax MCP can run ``understand_image`` (configured; optionally probed)."""
35
+ if not minimax_mcp_configured(base):
36
+ return False
37
+ if not probe:
38
+ return True
39
+ rows = servers_status
40
+ if rows is None:
41
+ from seed.integrations.mcp_client import get_mcp_manager
42
+
43
+ rows = get_mcp_manager().list_servers_status(probe=True)
44
+ for row in rows or []:
45
+ if str(row.get("id") or "") != MINIMAX_MCP_SERVER_ID:
46
+ continue
47
+ if not row.get("connected"):
48
+ return False
49
+ tools = row.get("tools") or []
50
+ return MINIMAX_UNDERSTAND_IMAGE_TOOL in tools
51
+ return False
52
+
53
+
54
+ def image_attachment_allowed(vision_llm_id: str, base: Optional[Path] = None) -> bool:
55
+ """User may send image attachments when a vision preset or MiniMax MCP is available."""
56
+ vid = (vision_llm_id or "").strip()
57
+ if vid == MCP_VISION_SENTINEL:
58
+ return minimax_mcp_configured(base)
59
+ if vid:
60
+ from codeagent.core.vision_models import preset_supports_vision_id
61
+
62
+ return preset_supports_vision_id(vid)
63
+ return minimax_mcp_configured(base)
64
+
65
+
66
+ def video_attachment_allowed(vision_llm_id: str) -> bool:
67
+ """Video analysis still requires a vision-capable LLM preset."""
68
+ from codeagent.core.vision_models import preset_supports_vision_id
69
+
70
+ vid = (vision_llm_id or "").strip()
71
+ return bool(vid) and preset_supports_vision_id(vid)
72
+
73
+
74
+ def image_understanding_status(
75
+ base: Optional[Path] = None,
76
+ *,
77
+ probe: bool = False,
78
+ servers_status: Optional[list[dict[str, Any]]] = None,
79
+ ) -> dict[str, Any]:
80
+ configured = minimax_mcp_configured(base)
81
+ ready = minimax_mcp_understand_image_ready(
82
+ base, probe=probe, servers_status=servers_status
83
+ )
84
+ return {
85
+ "configured": configured,
86
+ "ready": ready,
87
+ "sentinel": MCP_VISION_SENTINEL,
88
+ "mcp_tool": MCP_QUALIFIED_UNDERSTAND_IMAGE,
89
+ "minimax_server_id": MINIMAX_MCP_SERVER_ID,
90
+ }
@@ -0,0 +1,6 @@
1
+ """Core models (compat wrapper)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from seed.models import * # noqa: F403
6
+