@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.
Files changed (99) hide show
  1. package/package.json +2 -2
  2. package/vendor/agent-frontend-shell/.bitseek-source.json +2 -2
  3. package/vendor/agent-frontend-shell/CHANGELOG.md +178 -1
  4. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +5 -5
  5. package/vendor/agent-frontend-shell/api/agent_health.py +134 -0
  6. package/vendor/agent-frontend-shell/api/config.py +145 -104
  7. package/vendor/agent-frontend-shell/api/gateway_chat.py +56 -12
  8. package/vendor/agent-frontend-shell/api/helpers.py +4 -2
  9. package/vendor/agent-frontend-shell/api/models.py +202 -20
  10. package/vendor/agent-frontend-shell/api/paths.py +77 -0
  11. package/vendor/agent-frontend-shell/api/plugins.py +185 -0
  12. package/vendor/agent-frontend-shell/api/profiles.py +95 -16
  13. package/vendor/agent-frontend-shell/api/routes.py +831 -30
  14. package/vendor/agent-frontend-shell/api/run_journal.py +1 -0
  15. package/vendor/agent-frontend-shell/api/state_sync.py +5 -4
  16. package/vendor/agent-frontend-shell/api/streaming.py +211 -56
  17. package/vendor/agent-frontend-shell/api/todo_state.py +122 -0
  18. package/vendor/agent-frontend-shell/api/updates.py +30 -3
  19. package/vendor/agent-frontend-shell/api/upload.py +251 -18
  20. package/vendor/agent-frontend-shell/api/workspace.py +323 -65
  21. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_EN.docx +0 -0
  22. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_ZH.docx +0 -0
  23. package/vendor/agent-frontend-shell/bitseek_docs/en/00-Installation.md +174 -0
  24. package/vendor/agent-frontend-shell/bitseek_docs/en/01-Overview.md +128 -0
  25. package/vendor/agent-frontend-shell/bitseek_docs/en/02-Page-Operations.md +461 -0
  26. package/vendor/agent-frontend-shell/bitseek_docs/en/README.md +61 -0
  27. package/vendor/agent-frontend-shell/bitseek_docs/en/images/ai-colleagues.png +0 -0
  28. package/vendor/agent-frontend-shell/bitseek_docs/en/images/chat-area.png +0 -0
  29. package/vendor/agent-frontend-shell/bitseek_docs/en/images/kanban.png +0 -0
  30. package/vendor/agent-frontend-shell/bitseek_docs/en/images/main-page.png +0 -0
  31. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-notes.png +0 -0
  32. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-overview.png +0 -0
  33. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-profile.png +0 -0
  34. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-soul.png +0 -0
  35. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory.png +0 -0
  36. package/vendor/agent-frontend-shell/bitseek_docs/en/images/navigation-bar.png +0 -0
  37. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-appearance.png +0 -0
  38. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-conversation.png +0 -0
  39. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-overview.png +0 -0
  40. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-plugins.png +0 -0
  41. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-preferences.png +0 -0
  42. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-providers.png +0 -0
  43. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-system.png +0 -0
  44. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings.png +0 -0
  45. package/vendor/agent-frontend-shell/bitseek_docs/en/images/sidebar.png +0 -0
  46. package/vendor/agent-frontend-shell/bitseek_docs/en/images/skills.png +0 -0
  47. package/vendor/agent-frontend-shell/bitseek_docs/en/images/tasks.png +0 -0
  48. package/vendor/agent-frontend-shell/bitseek_docs/en/images/workspace-panel.png +0 -0
  49. package/vendor/agent-frontend-shell/bitseek_docs/md_to_docx.py +351 -0
  50. package/vendor/agent-frontend-shell/bitseek_docs/zh/00-/345/256/211/350/243/205/345/220/257/345/212/250.md +174 -0
  51. package/vendor/agent-frontend-shell/bitseek_docs/zh/01-/346/225/264/344/275/223/346/246/202/350/247/210.md +128 -0
  52. package/vendor/agent-frontend-shell/bitseek_docs/zh/02-/351/241/265/351/235/242/346/223/215/344/275/234.md +463 -0
  53. package/vendor/agent-frontend-shell/bitseek_docs/zh/README.md +61 -0
  54. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/ai-colleagues.png +0 -0
  55. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/chat-area.png +0 -0
  56. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/kanban.png +0 -0
  57. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/main-page.png +0 -0
  58. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-notes.png +0 -0
  59. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-overview.png +0 -0
  60. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-profile.png +0 -0
  61. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-soul.png +0 -0
  62. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory.png +0 -0
  63. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/navigation-bar.png +0 -0
  64. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-appearance.png +0 -0
  65. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-conversation.png +0 -0
  66. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-overview.png +0 -0
  67. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-plugins.png +0 -0
  68. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-preferences.png +0 -0
  69. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-providers.png +0 -0
  70. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-system.png +0 -0
  71. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings.png +0 -0
  72. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/sidebar.png +0 -0
  73. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/skills.png +0 -0
  74. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/tasks.png +0 -0
  75. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/workspace-panel.png +0 -0
  76. package/vendor/agent-frontend-shell/build-release.sh +62 -0
  77. package/vendor/agent-frontend-shell/ctl.sh +1 -0
  78. package/vendor/agent-frontend-shell/docker-compose.local.yml +33 -0
  79. package/vendor/agent-frontend-shell/docker-compose.yml +8 -0
  80. package/vendor/agent-frontend-shell/docker_init.bash +1 -0
  81. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +74 -15
  82. package/vendor/agent-frontend-shell/extensions/common/index.css +6 -0
  83. package/vendor/agent-frontend-shell/extensions/manifest.json +6 -0
  84. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +60 -14
  85. package/vendor/agent-frontend-shell/readme-simple.md +103 -0
  86. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  87. package/vendor/agent-frontend-shell/server.py +7 -0
  88. package/vendor/agent-frontend-shell/static/boot.js +53 -1
  89. package/vendor/agent-frontend-shell/static/commands.js +20 -10
  90. package/vendor/agent-frontend-shell/static/i18n.js +1142 -1016
  91. package/vendor/agent-frontend-shell/static/index.html +13 -3
  92. package/vendor/agent-frontend-shell/static/messages.js +48 -3
  93. package/vendor/agent-frontend-shell/static/panels.js +199 -30
  94. package/vendor/agent-frontend-shell/static/sessions.js +249 -39
  95. package/vendor/agent-frontend-shell/static/style.css +46 -2
  96. package/vendor/agent-frontend-shell/static/ui.js +323 -79
  97. package/vendor/agent-frontend-shell/static/workspace.js +185 -7
  98. package/vendor/agent-frontend-shell/README-CUSTOM.md +0 -76
  99. 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
- HOME = Path.home()
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
- Native Windows Hermes Agent installs default to %LOCALAPPDATA%\\hermes,
68
- while POSIX installs use ~/.hermes.
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
- Windows migration safety (#2905): v0.51.134 moved the Windows default from
71
- ``%USERPROFILE%\\.hermes`` to ``%LOCALAPPDATA%\\hermes`` to match the agent.
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
- source.strip().lower() in _AMBIENT_GH_CLI_MARKERS
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
- # Custom API aggregators (e.g. New API, One API) use non-standard model naming:
2188
- # bare names like "deepseek-v4-flash" or dot-separated "moonshotai.kimi-k2.5"
2189
- # rather than the OpenRouter-style "vendor/model" that the prefix list targets.
2190
- # Strip a dot-vendor prefix (e.g. "moonshotai.kimi-k2.5" "kimi-k2.5") and
2191
- # check both the original bare name and the stripped suffix.
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
- _norm = lambda s: (s.split("/", 1)[-1] if "/" in s else s).replace("-", ".").lower()
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: capitalize each hyphen-separated word, preserve dots in version numbers.
3106
- # The catalog lookup above handles well-known models; this only fires for unlisted IDs.
3107
- bare = lookup_id.split("/")[-1] if "/" in lookup_id else lookup_id
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
- # Strip provider/model prefix (e.g., custom:jingdong/GLM-5 -> GLM-5).
3261
- # Same trailing-empty guard.
3262
- if "/" in s:
3263
- parts = s.split("/")
3264
- s = parts[-1] or s
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": True, # check if webui/agent repos are behind upstream
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
- prefill_messages = _prefill_messages_with_webui_context(prefill_context, cfg)
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
- display = list(getattr(s, "messages", None) or [])
384
- # Avoid duplicating the eager-save checkpointed user message.
385
- if display:
386
- latest = display[-1]
387
- if isinstance(latest, dict) and latest.get("role") == "user":
388
- latest_text = " ".join(str(latest.get("content") or "").split())
389
- msg_norm = " ".join(str(msg_text or "").split())
390
- if latest_text == msg_norm:
391
- display = display[:-1]
392
- s.messages = display + [user_msg, assistant_msg]
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 and tool_call data before API response.
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