@bitseek/hermes-webui 0.1.0-beta.0 → 0.1.0
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/package.json +2 -2
- package/vendor/agent-frontend-shell/.bitseek-source.json +2 -2
- package/vendor/agent-frontend-shell/CHANGELOG.md +178 -1
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +5 -5
- package/vendor/agent-frontend-shell/api/agent_health.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +145 -104
- package/vendor/agent-frontend-shell/api/gateway_chat.py +56 -12
- package/vendor/agent-frontend-shell/api/helpers.py +4 -2
- package/vendor/agent-frontend-shell/api/models.py +202 -20
- package/vendor/agent-frontend-shell/api/paths.py +77 -0
- package/vendor/agent-frontend-shell/api/plugins.py +185 -0
- package/vendor/agent-frontend-shell/api/profiles.py +95 -16
- package/vendor/agent-frontend-shell/api/routes.py +831 -30
- package/vendor/agent-frontend-shell/api/run_journal.py +1 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +5 -4
- package/vendor/agent-frontend-shell/api/streaming.py +211 -56
- package/vendor/agent-frontend-shell/api/todo_state.py +122 -0
- package/vendor/agent-frontend-shell/api/updates.py +30 -3
- package/vendor/agent-frontend-shell/api/upload.py +251 -18
- package/vendor/agent-frontend-shell/api/workspace.py +323 -65
- package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_EN.docx +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_ZH.docx +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/00-Installation.md +174 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/01-Overview.md +128 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/02-Page-Operations.md +461 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/README.md +61 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/ai-colleagues.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/chat-area.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/kanban.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/main-page.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-notes.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-profile.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-soul.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/navigation-bar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-appearance.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-conversation.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-plugins.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-preferences.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-providers.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-system.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/sidebar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/skills.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/tasks.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/workspace-panel.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/md_to_docx.py +351 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/00-/345/256/211/350/243/205/345/220/257/345/212/250.md +174 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/01-/346/225/264/344/275/223/346/246/202/350/247/210.md +128 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/02-/351/241/265/351/235/242/346/223/215/344/275/234.md +463 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/README.md +61 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/ai-colleagues.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/chat-area.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/kanban.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/main-page.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-notes.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-profile.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-soul.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/navigation-bar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-appearance.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-conversation.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-plugins.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-preferences.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-providers.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-system.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/sidebar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/skills.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/tasks.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/workspace-panel.png +0 -0
- package/vendor/agent-frontend-shell/build-release.sh +62 -0
- package/vendor/agent-frontend-shell/ctl.sh +1 -0
- package/vendor/agent-frontend-shell/docker-compose.local.yml +33 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +8 -0
- package/vendor/agent-frontend-shell/docker_init.bash +1 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +74 -15
- package/vendor/agent-frontend-shell/extensions/common/index.css +6 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +60 -14
- package/vendor/agent-frontend-shell/readme-simple.md +103 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +7 -0
- package/vendor/agent-frontend-shell/static/boot.js +53 -1
- package/vendor/agent-frontend-shell/static/commands.js +20 -10
- package/vendor/agent-frontend-shell/static/i18n.js +1142 -1016
- package/vendor/agent-frontend-shell/static/index.html +13 -3
- package/vendor/agent-frontend-shell/static/messages.js +48 -3
- package/vendor/agent-frontend-shell/static/panels.js +199 -30
- package/vendor/agent-frontend-shell/static/sessions.js +249 -39
- package/vendor/agent-frontend-shell/static/style.css +46 -2
- package/vendor/agent-frontend-shell/static/ui.js +323 -79
- package/vendor/agent-frontend-shell/static/workspace.js +185 -7
- package/vendor/agent-frontend-shell/README-CUSTOM.md +0 -76
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +0 -26
|
@@ -26,75 +26,14 @@ from pathlib import Path
|
|
|
26
26
|
from urllib.parse import parse_qs, urlparse
|
|
27
27
|
|
|
28
28
|
# ── Basic layout ──────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
# REPO_ROOT is the directory that contains this file's parent (api/ -> repo root)
|
|
31
|
-
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _hermes_home_has_webui_state(base: Path) -> bool:
|
|
35
|
-
"""Return True when *base* holds real WebUI state under its ``webui/`` dir.
|
|
36
|
-
|
|
37
|
-
Used only on Windows to detect a pre-v0.51.134 install at the legacy
|
|
38
|
-
``%USERPROFILE%\\.hermes`` location so we don't strand the user's existing
|
|
39
|
-
sessions/pins/settings when the default moved to ``%LOCALAPPDATA%\\hermes``
|
|
40
|
-
(#2905).
|
|
41
|
-
|
|
42
|
-
We intentionally check ONLY WebUI-owned artifacts (the ``webui/`` subtree),
|
|
43
|
-
NOT agent-owned files like ``config.yaml`` / ``auth.json``. The agent has
|
|
44
|
-
defaulted to ``%LOCALAPPDATA%\\hermes`` on Windows since before #2897, so a
|
|
45
|
-
long-time agent user who never ran WebUI at the legacy location would have a
|
|
46
|
-
stray ``auth.json`` there — keying on that would wrongly divert a *fresh*
|
|
47
|
-
WebUI install to the legacy dir. Only ``webui/`` state is what actually
|
|
48
|
-
gets stranded by the move, so it is the correct and narrow signal.
|
|
49
|
-
Cheap stat-only checks; never raises.
|
|
50
|
-
"""
|
|
51
|
-
try:
|
|
52
|
-
if not base.is_dir():
|
|
53
|
-
return False
|
|
54
|
-
markers = (
|
|
55
|
-
base / "webui" / "sessions", # WebUI session store
|
|
56
|
-
base / "webui" / "settings.json", # WebUI UI settings + pins
|
|
57
|
-
base / "webui", # WebUI state dir at all
|
|
58
|
-
)
|
|
59
|
-
return any(m.exists() for m in markers)
|
|
60
|
-
except OSError:
|
|
61
|
-
return False
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _platform_default_hermes_home() -> Path:
|
|
65
|
-
"""Return the platform-aware default Hermes home when HERMES_HOME is unset.
|
|
29
|
+
import api.paths as _paths
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
|
|
31
|
+
HOME = _paths.HOME
|
|
32
|
+
_hermes_home_has_webui_state = _paths._hermes_home_has_webui_state
|
|
33
|
+
_platform_default_hermes_home = _paths._platform_default_hermes_home
|
|
69
34
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Upgrading users whose WebUI state still lives at the old location saw an
|
|
73
|
-
empty app (sessions/pins/settings "lost" — actually just at an address the
|
|
74
|
-
new build no longer reads). To avoid stranding that data, prefer the
|
|
75
|
-
legacy ``%USERPROFILE%\\.hermes`` ONLY when it is populated AND the new
|
|
76
|
-
``%LOCALAPPDATA%\\hermes`` location is not yet established. This is a
|
|
77
|
-
non-destructive, self-healing fallback: no files are moved, and once the
|
|
78
|
-
new location has state (fresh installs, or users who set HERMES_HOME) the
|
|
79
|
-
legacy path is never preferred. Explicit HERMES_HOME / HERMES_WEBUI_STATE_DIR
|
|
80
|
-
overrides take precedence upstream and are unaffected.
|
|
81
|
-
"""
|
|
82
|
-
if os.name == "nt":
|
|
83
|
-
local_app_data = os.getenv("LOCALAPPDATA", "").strip()
|
|
84
|
-
if local_app_data:
|
|
85
|
-
new_home = Path(local_app_data) / "hermes"
|
|
86
|
-
legacy_home = HOME / ".hermes"
|
|
87
|
-
# Only fall back to the legacy home if it actually holds state and
|
|
88
|
-
# the new location has not been established yet — the exact
|
|
89
|
-
# post-upgrade fingerprint from #2905.
|
|
90
|
-
if (
|
|
91
|
-
legacy_home != new_home
|
|
92
|
-
and not _hermes_home_has_webui_state(new_home)
|
|
93
|
-
and _hermes_home_has_webui_state(legacy_home)
|
|
94
|
-
):
|
|
95
|
-
return legacy_home
|
|
96
|
-
return new_home
|
|
97
|
-
return HOME / ".hermes"
|
|
35
|
+
# REPO_ROOT is the directory that contains this file's parent (api/ -> repo root)
|
|
36
|
+
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
98
37
|
|
|
99
38
|
# ── Network config (env-overridable) ─────────────────────────────────────────
|
|
100
39
|
HOST = os.getenv("HERMES_WEBUI_HOST", "127.0.0.1")
|
|
@@ -1311,14 +1250,29 @@ _PROVIDER_MODELS = {
|
|
|
1311
1250
|
|
|
1312
1251
|
_AMBIENT_GH_CLI_MARKERS = frozenset({"gh_cli", "gh auth token"})
|
|
1313
1252
|
|
|
1253
|
+
# Environment variable sources that are auto-detected and should be filtered
|
|
1254
|
+
# when the token is a classic PAT (ghp_*) that Copilot API doesn't support.
|
|
1255
|
+
# Note: COPILOT_GITHUB_TOKEN is NOT included here - it's user-specific config.
|
|
1256
|
+
_AMBIENT_GH_ENV_SOURCES = frozenset({"env:github_token", "env:gh_token"})
|
|
1257
|
+
|
|
1314
1258
|
|
|
1315
1259
|
def _is_ambient_gh_cli_entry(source: str, label: str, key_source: str) -> bool:
|
|
1316
1260
|
"""True when a credential-pool entry is a seeded gh-cli token rather than
|
|
1317
1261
|
one the user added explicitly. Filter these so Copilot doesn't appear in
|
|
1318
1262
|
the dropdown just because `gh` is installed on the system.
|
|
1263
|
+
|
|
1264
|
+
Also filters GITHUB_TOKEN and GH_TOKEN env var entries, which are
|
|
1265
|
+
auto-detected from the environment and should not cause Copilot to appear
|
|
1266
|
+
in the picker when the token is a classic PAT (ghp_*) that Copilot API
|
|
1267
|
+
doesn't support.
|
|
1268
|
+
|
|
1269
|
+
Note: COPILOT_GITHUB_TOKEN is NOT filtered - it's user-specific config
|
|
1270
|
+
that should always be respected.
|
|
1319
1271
|
"""
|
|
1272
|
+
source_lower = source.strip().lower()
|
|
1320
1273
|
return (
|
|
1321
|
-
|
|
1274
|
+
source_lower in _AMBIENT_GH_CLI_MARKERS
|
|
1275
|
+
or source_lower in _AMBIENT_GH_ENV_SOURCES
|
|
1322
1276
|
or label.strip().lower() == "gh auth token"
|
|
1323
1277
|
or key_source.strip().lower() == "gh auth token"
|
|
1324
1278
|
)
|
|
@@ -2155,6 +2109,90 @@ def _strip_provider_hint_for_reasoning(model_id: str) -> str:
|
|
|
2155
2109
|
return model
|
|
2156
2110
|
|
|
2157
2111
|
|
|
2112
|
+
def _reasoning_name_candidates(model_id: str) -> list[str]:
|
|
2113
|
+
"""Return normalized model-name candidates for heuristic capability checks."""
|
|
2114
|
+
bare = str(model_id or "").strip().lower().rsplit("/", 1)[-1]
|
|
2115
|
+
if not bare:
|
|
2116
|
+
return []
|
|
2117
|
+
|
|
2118
|
+
candidates: list[str] = []
|
|
2119
|
+
|
|
2120
|
+
def _add(value: str) -> None:
|
|
2121
|
+
candidate = str(value or "").strip().lower()
|
|
2122
|
+
if candidate and candidate not in candidates:
|
|
2123
|
+
candidates.append(candidate)
|
|
2124
|
+
|
|
2125
|
+
_add(bare)
|
|
2126
|
+
|
|
2127
|
+
dot_parts = [part for part in bare.split(".") if part]
|
|
2128
|
+
if len(dot_parts) > 1:
|
|
2129
|
+
# Try progressively stripping dot-separated vendor namespaces so inputs like
|
|
2130
|
+
# "moonshotai.kimi-k2.5" and "vendor.deepseek.v3.2" both surface the real
|
|
2131
|
+
# model family rather than treating every dot as part of the provider slug.
|
|
2132
|
+
for index in range(1, len(dot_parts)):
|
|
2133
|
+
suffix = ".".join(dot_parts[index:])
|
|
2134
|
+
if any(ch.isalpha() for ch in suffix):
|
|
2135
|
+
_add(suffix)
|
|
2136
|
+
|
|
2137
|
+
for candidate in list(candidates):
|
|
2138
|
+
normalized = re.sub(r"[^a-z0-9]+", "-", candidate).strip("-")
|
|
2139
|
+
_add(normalized)
|
|
2140
|
+
|
|
2141
|
+
return candidates
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def _candidate_supports_reasoning(candidate: str) -> bool:
|
|
2145
|
+
normalized = re.sub(r"[^a-z0-9]+", "-", str(candidate or "").strip().lower()).strip("-")
|
|
2146
|
+
if not normalized:
|
|
2147
|
+
return False
|
|
2148
|
+
|
|
2149
|
+
tokens = [token for token in normalized.split("-") if token]
|
|
2150
|
+
token_set = set(tokens)
|
|
2151
|
+
|
|
2152
|
+
if "thinking" in token_set or "reasoning" in token_set:
|
|
2153
|
+
return True
|
|
2154
|
+
if "gpt" in token_set or normalized.startswith("gpt"):
|
|
2155
|
+
# Restrict to GPT-5+ (exclude GPT-4o/4.1/3.5 — reasoning_effort unsupported)
|
|
2156
|
+
m = re.search(r"gpt-(\d+)", normalized)
|
|
2157
|
+
if m and int(m.group(1)) >= 5:
|
|
2158
|
+
return True
|
|
2159
|
+
return False
|
|
2160
|
+
if normalized in {"o1", "o3", "o4"} or normalized.startswith(("o1-", "o3-", "o4-")):
|
|
2161
|
+
return True
|
|
2162
|
+
if "claude" in token_set or normalized.startswith("claude"):
|
|
2163
|
+
# Restrict to Claude 4+ or Claude 3.7+ (exclude Claude 3.0/3.5)
|
|
2164
|
+
match = re.search(r"claude.*?(\d+)(?:\D+(\d+))?", normalized)
|
|
2165
|
+
if match:
|
|
2166
|
+
major = int(match.group(1))
|
|
2167
|
+
minor = int(match.group(2)) if match.group(2) else 0
|
|
2168
|
+
if major >= 4 or (major == 3 and minor >= 7):
|
|
2169
|
+
return True
|
|
2170
|
+
return False
|
|
2171
|
+
if "qwen" in token_set or normalized.startswith("qwen"):
|
|
2172
|
+
# Restrict to Qwen 3+ (exclude Qwen 2/2.5)
|
|
2173
|
+
match = re.search(r"qwen.*?(\d+)(?:\D+(\d+))?", normalized)
|
|
2174
|
+
if match:
|
|
2175
|
+
major = int(match.group(1))
|
|
2176
|
+
if major >= 3:
|
|
2177
|
+
return True
|
|
2178
|
+
return False
|
|
2179
|
+
if "kimi" in token_set or normalized.startswith("kimi"):
|
|
2180
|
+
return True
|
|
2181
|
+
if "minimax" in token_set or normalized.startswith("minimax"):
|
|
2182
|
+
return True
|
|
2183
|
+
if "mimo" in token_set or normalized.startswith("mimo"):
|
|
2184
|
+
return True
|
|
2185
|
+
if "glm" in token_set or normalized.startswith("glm"):
|
|
2186
|
+
return True
|
|
2187
|
+
if "step" in token_set or normalized.startswith("step"):
|
|
2188
|
+
return True
|
|
2189
|
+
if normalized.startswith(("deepseek-v", "deepseek-r")):
|
|
2190
|
+
return True
|
|
2191
|
+
if len(tokens) >= 2 and tokens[0] == "deepseek" and tokens[1].startswith(("v", "r")):
|
|
2192
|
+
return True
|
|
2193
|
+
return False
|
|
2194
|
+
|
|
2195
|
+
|
|
2158
2196
|
def _heuristic_reasoning_efforts(model_id: str, provider_id: str) -> list[str]:
|
|
2159
2197
|
"""Fallback when hermes_cli is unavailable."""
|
|
2160
2198
|
model = _strip_provider_hint_for_reasoning(model_id).lower()
|
|
@@ -2184,31 +2222,11 @@ def _heuristic_reasoning_efforts(model_id: str, provider_id: str) -> list[str]:
|
|
|
2184
2222
|
)
|
|
2185
2223
|
if any(model.startswith(prefix) for prefix in prefixes):
|
|
2186
2224
|
return list(VALID_REASONING_EFFORTS)
|
|
2187
|
-
#
|
|
2188
|
-
#
|
|
2189
|
-
#
|
|
2190
|
-
#
|
|
2191
|
-
|
|
2192
|
-
bare_after_dot = bare.split(".", 1)[-1] if "." in bare else bare
|
|
2193
|
-
thinking_bare_prefixes = (
|
|
2194
|
-
"deepseek-v4",
|
|
2195
|
-
"deepseek-r1",
|
|
2196
|
-
"deepseek-r2",
|
|
2197
|
-
"kimi-k2",
|
|
2198
|
-
"kimi-thinking",
|
|
2199
|
-
"qwen3",
|
|
2200
|
-
"claude-3",
|
|
2201
|
-
"claude-4",
|
|
2202
|
-
"o1-",
|
|
2203
|
-
"o3-",
|
|
2204
|
-
"o4-",
|
|
2205
|
-
)
|
|
2206
|
-
if any(
|
|
2207
|
-
bare.startswith(p) or bare_after_dot.startswith(p)
|
|
2208
|
-
for p in thinking_bare_prefixes
|
|
2209
|
-
):
|
|
2210
|
-
return list(VALID_REASONING_EFFORTS)
|
|
2211
|
-
if "thinking" in bare or "reasoning" in bare:
|
|
2225
|
+
# Named custom providers often rewrite model ids with dots, underscores, or
|
|
2226
|
+
# extra vendor namespaces. Normalize those shapes before applying family-level
|
|
2227
|
+
# reasoning heuristics so "deepseek.v3.2", "deepseek_v4_flash", and
|
|
2228
|
+
# "vendor.deepseek.v3.2" are treated consistently.
|
|
2229
|
+
if any(_candidate_supports_reasoning(candidate) for candidate in _reasoning_name_candidates(bare)):
|
|
2212
2230
|
return list(VALID_REASONING_EFFORTS)
|
|
2213
2231
|
return []
|
|
2214
2232
|
|
|
@@ -3094,17 +3112,20 @@ def _get_label_for_model(model_id: str, existing_groups: list) -> str:
|
|
|
3094
3112
|
if lookup_id.startswith("@") and ":" in lookup_id:
|
|
3095
3113
|
lookup_id = lookup_id.split(":", 1)[1]
|
|
3096
3114
|
|
|
3097
|
-
# Check existing groups for a matching label
|
|
3098
|
-
|
|
3115
|
+
# Check existing groups for a matching label.
|
|
3116
|
+
# Skip slash stripping for URI-scheme IDs (e.g. gpt://folder/model) (#3429).
|
|
3117
|
+
_has_scheme = lambda s: "://" in s
|
|
3118
|
+
_norm = lambda s: (s.split("/", 1)[-1] if ("/" in s and not _has_scheme(s)) else s).replace("-", ".").lower()
|
|
3099
3119
|
norm_lookup = _norm(lookup_id)
|
|
3100
3120
|
for g in existing_groups:
|
|
3101
3121
|
for m in g.get("models", []):
|
|
3102
3122
|
if m.get("label") and _norm(str(m.get("id", ""))) == norm_lookup:
|
|
3103
3123
|
return m["label"]
|
|
3104
3124
|
|
|
3105
|
-
# Fall back:
|
|
3106
|
-
#
|
|
3107
|
-
|
|
3125
|
+
# Fall back: strip only the first slash-segment (provider prefix),
|
|
3126
|
+
# preserving vendor hierarchy for multi-slash IDs (#3360).
|
|
3127
|
+
# Skip for URI-scheme IDs whose slashes are path separators (#3429).
|
|
3128
|
+
bare = lookup_id.split("/", 1)[1] if ("/" in lookup_id and not _has_scheme(lookup_id)) else lookup_id
|
|
3108
3129
|
return " ".join(
|
|
3109
3130
|
w.upper() if (len(w) <= 3 and w.replace(".", "").isalnum() and not w.isdigit()) else w.capitalize()
|
|
3110
3131
|
for w in bare.replace("_", "-").split("-")
|
|
@@ -3257,11 +3278,18 @@ def get_available_models() -> dict:
|
|
|
3257
3278
|
if s.startswith("@") and ":" in s:
|
|
3258
3279
|
parts = s.split(":")
|
|
3259
3280
|
s = parts[-1] or s
|
|
3260
|
-
#
|
|
3261
|
-
#
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3281
|
+
# Skip slash-based stripping for URI-scheme IDs (e.g.
|
|
3282
|
+
# gpt://folder/model/latest) whose slashes are path separators,
|
|
3283
|
+
# not provider delimiters (#3429).
|
|
3284
|
+
if "://" not in s:
|
|
3285
|
+
# Strip only the first slash-segment (provider prefix), preserving
|
|
3286
|
+
# any remaining vendor hierarchy. Using parts[-1] here previously
|
|
3287
|
+
# discarded ALL segments except the last, collapsing distinct
|
|
3288
|
+
# multi-slash IDs like 'vendor_a/deepseek-v4-pro' and
|
|
3289
|
+
# 'vendor_b/deepseek/deepseek-v4-pro' to the same key (#3360).
|
|
3290
|
+
if "/" in s:
|
|
3291
|
+
stripped = s.split("/", 1)[1]
|
|
3292
|
+
s = stripped or s
|
|
3265
3293
|
return s.replace("-", ".")
|
|
3266
3294
|
|
|
3267
3295
|
def _build_configured_model_badges() -> dict[str, dict[str, str]]:
|
|
@@ -4867,7 +4895,7 @@ _SETTINGS_DEFAULTS = {
|
|
|
4867
4895
|
"show_cli_sessions": False, # merge CLI sessions from state.db into the sidebar
|
|
4868
4896
|
"show_previous_messaging_sessions": False, # show older Telegram/Discord/etc. reset segments
|
|
4869
4897
|
"sync_to_insights": False, # mirror WebUI token usage to state.db for /insights
|
|
4870
|
-
"check_for_updates":
|
|
4898
|
+
"check_for_updates": False, # check if webui/agent repos are behind upstream
|
|
4871
4899
|
"ignore_agent_updates": False, # keep WebUI update notices but suppress Agent update checks
|
|
4872
4900
|
"whats_new_summary_enabled": False, # show an LLM-written What's New summary before diff links
|
|
4873
4901
|
"theme": "dark", # light | dark | system
|
|
@@ -4892,6 +4920,7 @@ _SETTINGS_DEFAULTS = {
|
|
|
4892
4920
|
"show_thinking": True, # show/hide thinking/reasoning blocks in chat view
|
|
4893
4921
|
"simplified_tool_calling": True, # render tools/thinking as compact inline timeline activity
|
|
4894
4922
|
"api_redact_enabled": True, # redact sensitive data (API keys, secrets) from API responses
|
|
4923
|
+
"dashboard_plugins": {}, # plugin_name -> bool, opt-in per plugin (default off per PF-10b)
|
|
4895
4924
|
"sidebar_density": "compact", # compact | detailed
|
|
4896
4925
|
"auto_title_refresh_every": "0", # adaptive title refresh: 0=off, 5/10/20=every N exchanges
|
|
4897
4926
|
"busy_input_mode": "queue", # behavior when sending while agent is running: queue | interrupt | steer
|
|
@@ -5063,7 +5092,19 @@ def save_settings(settings: dict) -> dict:
|
|
|
5063
5092
|
if settings.pop("_clear_password", False):
|
|
5064
5093
|
current["password_hash"] = None
|
|
5065
5094
|
_password_changed = True
|
|
5095
|
+
# Deep-merge dashboard_plugins dict (plugin_name -> bool)
|
|
5096
|
+
_dashboard_plugins = settings.get("dashboard_plugins")
|
|
5097
|
+
if isinstance(_dashboard_plugins, dict):
|
|
5098
|
+
current_dash = current.get("dashboard_plugins", {})
|
|
5099
|
+
if isinstance(current_dash, dict):
|
|
5100
|
+
# Coerce values to bool + keep only str keys so settings.json can't be
|
|
5101
|
+
# polluted with non-bool/non-str junk from a crafted POST.
|
|
5102
|
+
current_dash.update({k: bool(v) for k, v in _dashboard_plugins.items() if isinstance(k, str)})
|
|
5103
|
+
current["dashboard_plugins"] = current_dash
|
|
5066
5104
|
for k, v in settings.items():
|
|
5105
|
+
# dashboard_plugins is deep-merged above (not a flat allowlisted scalar).
|
|
5106
|
+
if k == "dashboard_plugins":
|
|
5107
|
+
continue
|
|
5067
5108
|
if k in _SETTINGS_ALLOWED_KEYS:
|
|
5068
5109
|
if k == "theme":
|
|
5069
5110
|
if isinstance(v, str) and v.strip():
|
|
@@ -24,7 +24,7 @@ from api.config import (
|
|
|
24
24
|
update_active_run,
|
|
25
25
|
)
|
|
26
26
|
from api.helpers import _redact_text, redact_session_data
|
|
27
|
-
from api.models import get_session
|
|
27
|
+
from api.models import get_session, merge_session_messages_append_only
|
|
28
28
|
from api.run_journal import RunJournalWriter
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
@@ -250,10 +250,29 @@ def _run_gateway_chat_streaming(
|
|
|
250
250
|
_load_webui_prefill_context,
|
|
251
251
|
_prefill_messages_with_webui_context,
|
|
252
252
|
_public_prefill_context_status,
|
|
253
|
+
_webui_ephemeral_system_prompt,
|
|
253
254
|
)
|
|
254
255
|
|
|
255
256
|
prefill_context = _load_webui_prefill_context(cfg)
|
|
256
|
-
|
|
257
|
+
# #3324: the WebUI session/delivery context (connected platforms,
|
|
258
|
+
# home channels, delivery hints, session framing) is now carried in
|
|
259
|
+
# the ephemeral system prompt rather than a prefill `user` message.
|
|
260
|
+
# The gateway-backed path must build the SAME system prompt so that
|
|
261
|
+
# context is not silently dropped on Gateway-routed WebUI chats.
|
|
262
|
+
_gateway_system_prompt = _webui_ephemeral_system_prompt(
|
|
263
|
+
None,
|
|
264
|
+
surface_context={
|
|
265
|
+
"source": "webui",
|
|
266
|
+
"session_id": session_id,
|
|
267
|
+
"profile": getattr(s, "profile", None),
|
|
268
|
+
"workspace": s.workspace if s is not None else str(workspace),
|
|
269
|
+
},
|
|
270
|
+
config_data=cfg,
|
|
271
|
+
)
|
|
272
|
+
prefill_messages = [
|
|
273
|
+
{"role": "system", "content": _gateway_system_prompt},
|
|
274
|
+
*_prefill_messages_with_webui_context(prefill_context, cfg),
|
|
275
|
+
]
|
|
257
276
|
put_gateway_event("context_status", {
|
|
258
277
|
"session_id": session_id,
|
|
259
278
|
"prefill": _public_prefill_context_status(prefill_context),
|
|
@@ -380,16 +399,41 @@ def _run_gateway_chat_streaming(
|
|
|
380
399
|
assistant_msg = {"role": "assistant", "content": assistant_text, "timestamp": assistant_ts}
|
|
381
400
|
previous_context = list(getattr(s, "context_messages", None) or getattr(s, "messages", None) or [])
|
|
382
401
|
s.context_messages = previous_context + [user_msg, assistant_msg]
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
402
|
+
try:
|
|
403
|
+
from api.streaming import _is_context_compression_marker
|
|
404
|
+
|
|
405
|
+
display_context = [
|
|
406
|
+
msg
|
|
407
|
+
for msg in previous_context
|
|
408
|
+
if not _is_context_compression_marker(msg)
|
|
409
|
+
]
|
|
410
|
+
except Exception:
|
|
411
|
+
logger.debug("Failed to filter gateway display context markers", exc_info=True)
|
|
412
|
+
display_context = previous_context
|
|
413
|
+
display = merge_session_messages_append_only(
|
|
414
|
+
list(getattr(s, "messages", None) or []),
|
|
415
|
+
display_context,
|
|
416
|
+
)
|
|
417
|
+
try:
|
|
418
|
+
from api.streaming import _merge_display_messages_after_agent_result
|
|
419
|
+
|
|
420
|
+
s.messages = _merge_display_messages_after_agent_result(
|
|
421
|
+
display,
|
|
422
|
+
previous_context,
|
|
423
|
+
s.context_messages,
|
|
424
|
+
str(msg_text or ""),
|
|
425
|
+
)
|
|
426
|
+
except Exception:
|
|
427
|
+
logger.debug("Failed to merge gateway display transcript", exc_info=True)
|
|
428
|
+
# Avoid duplicating the eager-save checkpointed user message.
|
|
429
|
+
if display:
|
|
430
|
+
latest = display[-1]
|
|
431
|
+
if isinstance(latest, dict) and latest.get("role") == "user":
|
|
432
|
+
latest_text = " ".join(str(latest.get("content") or "").split())
|
|
433
|
+
msg_norm = " ".join(str(msg_text or "").split())
|
|
434
|
+
if latest_text == msg_norm:
|
|
435
|
+
display = display[:-1]
|
|
436
|
+
s.messages = display + [user_msg, assistant_msg]
|
|
393
437
|
s.active_stream_id = None
|
|
394
438
|
s.pending_user_message = None
|
|
395
439
|
s.pending_attachments = None
|
|
@@ -367,9 +367,9 @@ def _redact_value(v, *, _enabled: bool | None = None):
|
|
|
367
367
|
|
|
368
368
|
|
|
369
369
|
def redact_session_data(session_dict: dict) -> dict:
|
|
370
|
-
"""Redact credentials from message content
|
|
370
|
+
"""Redact credentials from message content, tool data, and session sidecars.
|
|
371
371
|
|
|
372
|
-
Applies to: messages[], tool_calls[], and title.
|
|
372
|
+
Applies to: messages[], tool_calls[], todo_state, and title.
|
|
373
373
|
The underlying session file is not modified; redaction is response-layer only.
|
|
374
374
|
|
|
375
375
|
Reads the ``api_redact_enabled`` setting ONCE for the entire response and
|
|
@@ -387,6 +387,8 @@ def redact_session_data(session_dict: dict) -> dict:
|
|
|
387
387
|
result['messages'] = _redact_value(result['messages'], _enabled=_enabled)
|
|
388
388
|
if 'tool_calls' in result:
|
|
389
389
|
result['tool_calls'] = _redact_value(result['tool_calls'], _enabled=_enabled)
|
|
390
|
+
if 'todo_state' in result:
|
|
391
|
+
result['todo_state'] = _redact_value(result['todo_state'], _enabled=_enabled)
|
|
390
392
|
return result
|
|
391
393
|
|
|
392
394
|
|