jac-coder 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {jac_coder-0.2.1 → jac_coder-0.2.3}/PKG-INFO +1 -1
  2. {jac_coder-0.2.1 → jac_coder-0.2.3}/README.md +2 -0
  3. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/api.impl.jac +81 -49
  4. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/api.jac +3 -0
  5. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/core/nodes.impl.jac +13 -5
  6. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/core/nodes.jac +15 -1
  7. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/core/walkers.impl.jac +51 -33
  8. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/config.impl.jac +1 -0
  9. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/config.jac +1 -0
  10. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/mcp_manager.impl.jac +52 -20
  11. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/mcp_manager.jac +7 -0
  12. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/cost_tracker.impl.jac +6 -0
  13. jac_coder-0.2.3/jac_coder/runtime/file_logger.jac +235 -0
  14. jac_coder-0.2.3/jac_coder/runtime/permission.impl.jac +171 -0
  15. jac_coder-0.2.3/jac_coder/runtime/permission.jac +55 -0
  16. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/prompt.jac +16 -10
  17. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/skills.impl.jac +28 -4
  18. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/server.jac +58 -12
  19. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/ROADMAP.md +1 -0
  20. jac_coder-0.2.3/jac_coder/skills/jac-cl-auth/SKILL.md +134 -0
  21. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-components/SKILL.md +34 -10
  22. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-organization/SKILL.md +26 -1
  23. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-routing/SKILL.md +2 -5
  24. jac_coder-0.2.3/jac_coder/skills/jac-cl-styling/SKILL.md +51 -0
  25. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +2 -2
  26. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +2 -1
  27. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +24 -3
  28. jac_coder-0.2.3/jac_coder/skills/jac-npm-packages/SKILL.md +96 -0
  29. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-scaffold/SKILL.md +5 -6
  30. jac_coder-0.2.3/jac_coder/skills/jac-shadcn-components/SKILL.md +340 -0
  31. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-auth/SKILL.md +9 -9
  32. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-endpoints/SKILL.md +8 -8
  33. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-persistence/SKILL.md +15 -15
  34. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-types/SKILL.md +1 -0
  35. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-walker-patterns/SKILL.md +5 -5
  36. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/run/guarded.impl.jac +32 -2
  37. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/run/guarded.jac +4 -1
  38. jac_coder-0.2.3/jac_coder/tool/run/shell.impl.jac +206 -0
  39. jac_coder-0.2.3/jac_coder/tool/run/shell.jac +23 -0
  40. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/PKG-INFO +1 -1
  41. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/SOURCES.txt +4 -0
  42. {jac_coder-0.2.1 → jac_coder-0.2.3}/pyproject.toml +1 -1
  43. jac_coder-0.2.1/jac_coder/runtime/permission.impl.jac +0 -62
  44. jac_coder-0.2.1/jac_coder/runtime/permission.jac +0 -19
  45. jac_coder-0.2.1/jac_coder/skills/jac-cl-auth/SKILL.md +0 -93
  46. jac_coder-0.2.1/jac_coder/tool/run/shell.impl.jac +0 -152
  47. jac_coder-0.2.1/jac_coder/tool/run/shell.jac +0 -13
  48. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/__init__.jac +0 -0
  49. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/__init__.py +0 -0
  50. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/cli_entry.py +0 -0
  51. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/core/__init__.jac +0 -0
  52. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/core/walkers.jac +0 -0
  53. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/__init__.jac +0 -0
  54. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/infra/kv.jac +0 -0
  55. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/lib/__init__.jac +0 -0
  56. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/lib/coder.impl.jac +0 -0
  57. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/lib/coder.jac +0 -0
  58. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/__init__.jac +0 -0
  59. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/context.impl.jac +0 -0
  60. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/context.jac +0 -0
  61. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/cost_tracker.jac +0 -0
  62. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/events.jac +0 -0
  63. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/memory.impl.jac +0 -0
  64. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/memory.jac +0 -0
  65. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/prompt.impl.jac +0 -0
  66. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/runtime/skills.jac +0 -0
  67. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/serve_entry.jac +0 -0
  68. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
  69. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
  70. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
  71. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/__init__.jac +0 -0
  72. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/git.impl.jac +0 -0
  73. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/git.jac +0 -0
  74. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/mcp.impl.jac +0 -0
  75. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/mcp.jac +0 -0
  76. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/__init__.jac +0 -0
  77. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/delegation.impl.jac +0 -0
  78. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/delegation.jac +0 -0
  79. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/question.impl.jac +0 -0
  80. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/question.jac +0 -0
  81. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/task.impl.jac +0 -0
  82. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/task.jac +0 -0
  83. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/think.impl.jac +0 -0
  84. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/think.jac +0 -0
  85. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/todo.impl.jac +0 -0
  86. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/todo.jac +0 -0
  87. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/validate.impl.jac +0 -0
  88. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/meta/validate.jac +0 -0
  89. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/net/__init__.jac +0 -0
  90. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/net/preview.impl.jac +0 -0
  91. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/net/preview.jac +0 -0
  92. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/net/web.impl.jac +0 -0
  93. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/net/web.jac +0 -0
  94. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/__init__.jac +0 -0
  95. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/filesystem.impl.jac +0 -0
  96. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/filesystem.jac +0 -0
  97. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  98. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  99. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
  100. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/load_jac_skill.jac +0 -0
  101. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/search.impl.jac +0 -0
  102. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/read/search.jac +0 -0
  103. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/run/__init__.jac +0 -0
  104. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  105. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/run/jac_tools.jac +0 -0
  106. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/write/__init__.jac +0 -0
  107. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/write/checked.impl.jac +0 -0
  108. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/tool/write/checked.jac +0 -0
  109. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/__init__.jac +0 -0
  110. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/colors.jac +0 -0
  111. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/sandbox.impl.jac +0 -0
  112. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/sandbox.jac +0 -0
  113. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/tool_output.impl.jac +0 -0
  114. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder/util/tool_output.jac +0 -0
  115. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/dependency_links.txt +0 -0
  116. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/entry_points.txt +0 -0
  117. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/requires.txt +0 -0
  118. {jac_coder-0.2.1 → jac_coder-0.2.3}/jac_coder.egg-info/top_level.txt +0 -0
  119. {jac_coder-0.2.1 → jac_coder-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-coder
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: AI coding agent backend for Jac, powered by jac-byllm
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -156,3 +156,5 @@ jac-code/
156
156
  ├── vscode-jac-coder/ # VS Code extension (TypeScript)
157
157
  └── docs/ # ARCHITECTURE, ROADMAP, PROGRESS, LIBRARY_MODE
158
158
  ```
159
+
160
+ <img width="2661" height="1091" alt="image" src="https://github.com/user-attachments/assets/a314087f-ae94-4c8b-8899-1e4ff3eb465c" />
@@ -244,13 +244,6 @@ impl chat(
244
244
  # Memory initialization
245
245
  _ensure_memory(session, work_dir);
246
246
 
247
- # Add user message
248
- user_msg_record: dict = {"role": "user", "content": message};
249
- if images and len(images) > 0 {
250
- user_msg_record["has_images"] = True;
251
- user_msg_record["image_count"] = len(images);
252
- }
253
- session.chat_history.append(user_msg_record);
254
247
  session.updated_at = datetime.now().isoformat();
255
248
 
256
249
  # Get MainAgent — always resolve fresh (cached refs go stale in jac start)
@@ -260,15 +253,33 @@ impl chat(
260
253
  config = get_config();
261
254
  _init_spawn_budget(config.max_react_iterations * 3);
262
255
 
263
- # Build context
264
- ctx_history = build_context(
265
- session.chat_history,
266
- session.active_files,
267
- session.pending_errors,
268
- project_summary=session.project_summary or ""
269
- );
256
+ # Per-turn context gets folded into the user message; we can't add
257
+ # role=system dicts to session.chat_history without them persisting stale.
258
+ prefix_parts: list[str] = [];
259
+
260
+ if session.project_summary {
261
+ is_progress_file = session.project_summary.lstrip().startswith("#");
262
+ label = (
263
+ "Project progress (.jaccoder/progress.md)"
264
+ if is_progress_file
265
+ else "Project context"
266
+ );
267
+ prefix_parts.append(
268
+ f"[INTERNAL — do NOT present this to the user unless they ask about the project. This is your background knowledge for making informed decisions.]\n{label}:\n{session.project_summary}"
269
+ );
270
+ }
271
+ if session.active_files {
272
+ prefix_parts.append("Active files: " + ", ".join(session.active_files));
273
+ }
274
+ if session.pending_errors {
275
+ prefix_parts.append("Pending errors:\n" + "\n".join(session.pending_errors));
276
+ }
277
+
278
+ mcp_msg = _get_mcp_context_msg();
279
+ if mcp_msg {
280
+ prefix_parts.append(str(mcp_msg.get("content", "")));
281
+ }
270
282
 
271
- # Inject mode-aware workflow modules (dynamic prompt assembly)
272
283
  import from jac_coder.runtime.prompt { select_prompt_modules as _select_modules }
273
284
  workflow_modules = _select_modules(
274
285
  last_mode_hint=session.last_mode_hint,
@@ -277,20 +288,28 @@ impl chat(
277
288
  chat_history=session.chat_history
278
289
  );
279
290
  if workflow_modules {
280
- ctx_history.insert(0, {"role": "system", "content": workflow_modules});
291
+ prefix_parts.append(workflow_modules);
281
292
  }
282
293
 
283
- # Inject agent_context if provided (e.g. from JacBuilder)
284
294
  if agent_context {
285
- ctx_history.insert(0, {"role": "system", "content": agent_context});
295
+ prefix_parts.append(agent_context);
286
296
  }
287
297
 
288
- # Inject available MCP tools into context (rebuilt every turn)
289
- mcp_msg = _get_mcp_context_msg();
290
- if mcp_msg {
291
- ctx_history.insert(0, mcp_msg);
298
+ if edit_mode == "plan" {
299
+ prefix_parts.append(
300
+ "You are in PLAN MODE. Your task is to produce a detailed, step-by-step plan "
301
+ "for the user's request. Do NOT write, edit, or run any files. "
302
+ "Describe every file you would create or modify, what changes you would make, "
303
+ "and why. The user will review your plan and click 'Execute Plan' to apply it."
304
+ );
292
305
  }
293
306
 
307
+ composed_message = (
308
+ "\n\n---\n\n".join(prefix_parts) + "\n\n---\n\n" + message
309
+ if prefix_parts
310
+ else message
311
+ );
312
+
294
313
  # Early exit if already aborted before LLM starts
295
314
  if is_abort_requested() {
296
315
  tool_end();
@@ -306,24 +325,6 @@ impl chat(
306
325
  };
307
326
  }
308
327
 
309
- # Plan mode — inject instruction to write a plan only, no file writes.
310
- # The UI will show "Execute Plan" when done; execution reruns with edit_mode="auto".
311
- if edit_mode == "plan" {
312
- plan_msg: dict = {
313
- "role": "system",
314
- "content": (
315
- "You are in PLAN MODE. Your task is to produce a detailed, step-by-step plan "
316
- "for the user's request. Do NOT write, edit, or run any files. "
317
- "Describe every file you would create or modify, what changes you would make, "
318
- "and why. The user will review your plan and click 'Execute Plan' to apply it."
319
- )
320
- };
321
- ctx_with_plan: list[dict] = [plan_msg];
322
- ctx_with_plan.extend(ctx_history);
323
- ctx_history = ctx_with_plan;
324
- }
325
-
326
- # Call MainAgent — returns StreamEvent generator with logging enabled
327
328
  # If user attached images, convert to byllm Image object and pass as user_image
328
329
  user_image = None;
329
330
  if images and len(images) > 0 {
@@ -339,7 +340,10 @@ impl chat(
339
340
  sys.stderr.write(f"[api] Failed to create Image from user attachment: {img_err}\n");
340
341
  }
341
342
  }
342
- event_stream = main_agent.respond(message=message, chat_history=ctx_history, user_image=user_image);
343
+
344
+ main_agent._conv = session.chat_history;
345
+
346
+ event_stream = main_agent.respond(message=composed_message, user_image=user_image);
343
347
  stream_result = _consume_llm_stream(event_stream);
344
348
 
345
349
  response_text = stream_result["content"];
@@ -379,16 +383,22 @@ impl chat(
379
383
  }
380
384
  }
381
385
 
382
- # Persist response skip if aborted (abort note already in chat_history)
386
+ # byLLM already appended the assistant/tool turns; patch UI metadata
387
+ # onto the trailing assistant entry. Skip if aborted.
383
388
  if not is_abort_requested() {
384
- record: dict = {"role": "assistant", "content": response_text, "agent": "main"};
385
- if tools_used {
386
- record["tools_used"] = tools_used;
387
- }
388
- if files_modified {
389
- record["files_modified"] = files_modified;
389
+ for idx in range(len(session.chat_history) - 1, -1, -1) {
390
+ message_entry = session.chat_history[idx];
391
+ if message_entry.get("role") == "assistant" and "agent" not in message_entry {
392
+ message_entry["agent"] = "main";
393
+ if tools_used {
394
+ message_entry["tools_used"] = tools_used;
395
+ }
396
+ if files_modified {
397
+ message_entry["files_modified"] = files_modified;
398
+ }
399
+ break;
400
+ }
390
401
  }
391
- session.chat_history.append(record);
392
402
  }
393
403
  session.last_agent = "main";
394
404
  session.updated_at = datetime.now().isoformat();
@@ -514,6 +524,28 @@ impl api_get_model() -> dict {
514
524
  return _get_model();
515
525
  }
516
526
 
527
+ impl api_check_local_setup(alias: str) -> dict {
528
+ runtime: bool = False;
529
+ try {
530
+ import llama_cpp;
531
+ runtime = True;
532
+ } except ImportError { }
533
+
534
+ model: bool = False;
535
+ valid_alias: bool = False;
536
+ try {
537
+ import from byllm.local_runtime { LOCAL_MODELS }
538
+ import from byllm.model_cache { is_downloaded }
539
+ spec = LOCAL_MODELS.get(alias);
540
+ if spec is not None {
541
+ valid_alias = True;
542
+ model = is_downloaded(alias, spec);
543
+ }
544
+ } except Exception { }
545
+
546
+ return {"runtime": runtime, "model": model, "valid_alias": valid_alias};
547
+ }
548
+
517
549
 
518
550
  # ---------------------------------------------------------------------------
519
551
  # MCP management API
@@ -78,6 +78,9 @@ def api_set_model(model: str) -> dict;
78
78
  """Get current model info."""
79
79
  def api_get_model() -> dict;
80
80
 
81
+ """Check if local model runtime and model weights are ready for a given alias."""
82
+ def api_check_local_setup(alias: str) -> dict;
83
+
81
84
  # --- MCP management API ---
82
85
  """Add or update an MCP server."""
83
86
  def api_mcp_add(name: str, config: dict) -> dict;
@@ -140,11 +140,19 @@ Build breadth-first — get all files written and app running before polishing.
140
140
  ## Workflow
141
141
  1. Read .jaccoder/progress.md if it exists, update it as you go.
142
142
  2. analyze_project(dir) for existing projects.
143
- 3. jac.toml needs `[plugins.client.vite]` with tailwindcss. Run `jac install` after toml changes.
144
- 4. Build bottom-up: services styles/global.css hooks components → layout.
145
- 5. styles/global.css: `@import "tailwindcss"`, @theme tokens. In main.jac: `cl import ".styles.global.css";`
146
- 6. After all files: jac start --dev main.jac (background=True).
147
- 7. browser_validate(url) follow FAIL action instructions.
143
+ 3. Build bottom-up: services hooks components layout.
144
+ 4. **If `components/ui/` exists AND jac.toml has `[jac-shadcn]` (jac-shadcn project):**
145
+ - Load `jac-shadcn-components` skill BEFORE writing any UI.
146
+ - Load `jac-cl-styling` for conditional class and cn() patterns.
147
+ - `styles/global.css` and `jac.toml` are pre-configured do NOT recreate or modify them.
148
+ - Import UI from `components/ui/` instead of writing raw primitive components. No inline styles.
149
+ **Otherwise (standard project):**
150
+ - jac.toml needs `[plugins.client.vite]` with tailwindcss. Run `jac install` after toml changes.
151
+ - styles/global.css: `@import "tailwindcss"`, @theme tokens. In main.jac: `cl import ".styles.global.css";`
152
+ - Load `jac-cl-styling` when writing Tailwind classes or dynamic styles.
153
+ - Load `jac-npm-packages` when adding third-party npm packages.
154
+ 5. After all files: jac start --dev main.jac (background=True).
155
+ 6. browser_validate(url) → follow FAIL action instructions.
148
156
 
149
157
  Load the relevant `load_jac_skill(name)` from `<jac-skills-available>` BEFORE writing Jac. Tools catch obvious anti-patterns — trust BLOCKED messages.
150
158
  """;
@@ -82,6 +82,16 @@ obj SessionLearnings {
82
82
  # ---------------------------------------------------------------------------
83
83
  node McpRegistry {
84
84
  has servers: dict[str, dict] = {};
85
+ has disconnected_servers: list[str] = [];
86
+ }
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # UserPermissionRules — persists per-user "always allow" decisions in the graph
91
+ # so they are visible across pods (B3 / horizontal scaling).
92
+ node UserPermissionRules {
93
+ has user_id: str = "",
94
+ always_allowed: dict[str, list[str]] = {};
85
95
  }
86
96
 
87
97
 
@@ -145,7 +155,10 @@ node Session {
145
155
  # MainAgent — the orchestrator node
146
156
  # ---------------------------------------------------------------------------
147
157
  node MainAgent {
148
- def respond(message: str, chat_history: list[dict], user_image: Image | None = None) -> str by llm(
158
+ # Bound to Session.chat_history by the walker before each call.
159
+ has _conv: list[dict] = [];
160
+
161
+ def respond(message: str, user_image: Image | None = None) -> str by llm(
149
162
  tools=[
150
163
  # Think — explicit reasoning before acting or delegating
151
164
  think,
@@ -184,6 +197,7 @@ node MainAgent {
184
197
  # MCP — call tools from connected MCP servers
185
198
  mcp_call
186
199
  ],
200
+ conversation=self._conv,
187
201
  on_iteration=_iteration_hook,
188
202
  max_react_iterations=80,
189
203
  temperature=0.2,
@@ -173,8 +173,7 @@ impl interact.enter_session with Session entry {
173
173
  }
174
174
  }
175
175
 
176
- # Append user message and update
177
- here.chat_history.append({"role": "user", "content": self.message});
176
+ # byLLM appends the user message from the `respond(message=...)` arg.
178
177
  here.updated_at = datetime.now().isoformat();
179
178
  self.chat_history = here.chat_history;
180
179
 
@@ -212,28 +211,27 @@ impl _persist_response(
212
211
  files_modified: list[str] = [],
213
212
  tool_records: list[dict] = []
214
213
  ) -> None {
214
+ # byLLM already appended the user/tool/assistant turns via _conv binding;
215
+ # we only patch JacCoder UI metadata onto the trailing assistant entry.
215
216
  session = _find_session(session_id);
216
217
  if not session {
217
218
  return;
218
219
  }
219
220
 
220
- for rec in tool_records {
221
- session.chat_history.append({
222
- "role": "tool",
223
- "name": rec.get("tool", ""),
224
- "args": rec.get("args", {}),
225
- "content": rec.get("result", "")
226
- });
221
+ for idx in range(len(session.chat_history) - 1, -1, -1) {
222
+ message_entry = session.chat_history[idx];
223
+ if message_entry.get("role") == "assistant" and "agent" not in message_entry {
224
+ message_entry["agent"] = agent_mode;
225
+ if tools_used {
226
+ message_entry["tools_used"] = tools_used;
227
+ }
228
+ if files_modified {
229
+ message_entry["files_modified"] = files_modified;
230
+ }
231
+ break;
232
+ }
227
233
  }
228
234
 
229
- record: dict = {"role": "assistant", "content": response, "agent": agent_mode};
230
- if tools_used {
231
- record["tools_used"] = tools_used;
232
- }
233
- if files_modified {
234
- record["files_modified"] = files_modified;
235
- }
236
- session.chat_history.append(record);
237
235
  session.last_agent = agent_mode;
238
236
  session.updated_at = datetime.now().isoformat();
239
237
  }
@@ -314,45 +312,65 @@ impl _respond_and_persist(agent: MainAgent, ctx: interact) -> None {
314
312
  pending_errors = session.pending_errors if session else [];
315
313
  project_summary = session.project_summary if session else "";
316
314
 
317
- # Build compacted context
318
- ctx_history = build_context(
319
- ctx.chat_history, active_files, pending_errors, project_summary=project_summary
320
- );
315
+ # Per-turn context gets folded into the user message; we can't add
316
+ # role=system dicts to session.chat_history without them persisting stale.
317
+ prefix_parts: list[str] = [];
318
+
319
+ if project_summary {
320
+ is_progress_file = project_summary.lstrip().startswith("#");
321
+ label = (
322
+ "Project progress (.jaccoder/progress.md)"
323
+ if is_progress_file
324
+ else "Project context"
325
+ );
326
+ prefix_parts.append(
327
+ f"[INTERNAL — do NOT present this to the user unless they ask about the project. This is your background knowledge for making informed decisions.]\n{label}:\n{project_summary}"
328
+ );
329
+ }
330
+ if active_files {
331
+ prefix_parts.append("Active files: " + ", ".join(active_files));
332
+ }
333
+ if pending_errors {
334
+ prefix_parts.append("Pending errors:\n" + "\n".join(pending_errors));
335
+ }
321
336
 
322
- # Inject the available-skills listing (names + descriptions only).
323
- # The LLM picks which skills it needs and pulls bodies via load_jac_skill(name).
324
- # Inserted FIRST so workflow_modules ends up above it in the final prompt,
325
- # keeping the listing closer to the user message where attention is strongest.
326
337
  import from jac_coder.runtime.skills { inject_skills_listing }
327
338
  skill_listing = inject_skills_listing();
328
339
  if skill_listing {
329
- ctx_history.insert(0, {"role": "system", "content": skill_listing});
340
+ prefix_parts.append(skill_listing);
330
341
  }
331
342
 
332
- # Inject mode-aware workflow modules (dynamic prompt assembly)
333
343
  import from jac_coder.runtime.prompt { select_prompt_modules }
334
344
  workflow_modules = select_prompt_modules(
335
345
  last_mode_hint=session.last_mode_hint if session else "",
336
346
  message=ctx.message,
337
347
  directory=session.directory if session else "",
338
- chat_history=ctx.chat_history
348
+ chat_history=session.chat_history if session else []
339
349
  );
340
350
  if workflow_modules {
341
- ctx_history.insert(0, {"role": "system", "content": workflow_modules});
351
+ prefix_parts.append(workflow_modules);
342
352
  }
343
353
 
344
- # If agent_context was provided (e.g. from JacBuilder), inject it
345
354
  if ctx.agent_context {
346
- ctx_history.insert(0, {"role": "system", "content": ctx.agent_context});
355
+ prefix_parts.append(ctx.agent_context);
347
356
  }
348
357
 
358
+ composed_message = (
359
+ "\n\n---\n\n".join(prefix_parts) + "\n\n---\n\n" + ctx.message
360
+ if prefix_parts
361
+ else ctx.message
362
+ );
363
+
349
364
  # Initialize shared budget for SubAgents
350
365
  import from jac_coder.tool.meta.delegation { init_budget }
351
366
  config = get_config();
352
367
  init_budget(config.max_react_iterations * 3);
353
368
 
354
- # Call MainAgent — returns StreamEvent generator with logging enabled
355
- event_stream = agent.respond(message=ctx.message, chat_history=ctx_history);
369
+ if session {
370
+ agent._conv = session.chat_history;
371
+ }
372
+
373
+ event_stream = agent.respond(message=composed_message);
356
374
  stream_result = _consume_llm_stream(event_stream);
357
375
 
358
376
  response_text = stream_result["content"];
@@ -99,6 +99,7 @@ impl set_model(model: str) -> dict {
99
99
  }
100
100
  model_name = model;
101
101
  llm.model_name = model;
102
+ llm.postinit();
102
103
  _config.default_model = model;
103
104
  return {"status": "ok", "model": model};
104
105
  }
@@ -87,4 +87,5 @@ with entry {
87
87
  # with the hardcoded default before load_config() runs.
88
88
  model_name = _config.default_model;
89
89
  llm.model_name = model_name;
90
+ llm.postinit();
90
91
  }
@@ -30,12 +30,20 @@ glob _sessions: dict = {}; # name -> ClientSession (open inside _mgr_loop)
30
30
  glob _exit_stacks: dict = {}; # name -> AsyncExitStack (keeps transports alive)
31
31
  glob _TOOLS_CACHE_KEY: str = "jc:mcp:tools";
32
32
  glob _TOOLS_CACHE_TTL: int = 30; # seconds before tool list is re-fetched
33
- glob _builtin_names: set = set(); # names registered via mcp_register_builtin
34
- glob _disconnected_names: set = set(); # names intentionally disconnected by user
33
+
34
+ impl _service_mode_active() -> bool {
35
+ return bool(
36
+ os.environ.get("JACCODER_WEB_MODE", "")
37
+ or os.environ.get("JACCODER_IDE_MODE", "")
38
+ );
39
+ }
35
40
 
36
41
 
37
42
  """Background thread: check PyPI for latest jac-mcp version (3s timeout, fails silently)."""
38
- def _check_jac_mcp_version() -> None {
43
+ impl _check_jac_mcp_version() -> None {
44
+ if _service_mode_active() {
45
+ return;
46
+ }
39
47
  try {
40
48
  current: str = version("jac-mcp");
41
49
  req = Request(
@@ -65,7 +73,13 @@ def _get_registry() -> Any {
65
73
  target_anchor = edge_anchor.target;
66
74
  target_anchor.populate();
67
75
  if isinstance(target_anchor.archetype, McpRegistry) {
68
- return target_anchor.archetype;
76
+ reg = target_anchor.archetype;
77
+ # Migration guard: nodes persisted before disconnected_servers was
78
+ # added to the schema won't have this attribute after deserialization.
79
+ if not hasattr(reg, "disconnected_servers") {
80
+ reg.disconnected_servers = [];
81
+ }
82
+ return reg;
69
83
  }
70
84
  } except Exception { }
71
85
  }
@@ -286,31 +300,33 @@ impl mcp_add_server(name: str, config: dict) -> dict {
286
300
  }
287
301
 
288
302
 
289
- """Close the connection and remove the server from the registry."""
303
+ """Close the connection and mark the server disconnected in the graph."""
290
304
  impl mcp_disconnect_server(name: str) -> dict {
291
305
  # Disconnect only — close the active connection but keep the config.
292
306
  # This applies to ALL servers (builtin and user-added) so they stay
293
307
  # visible in the list and can be reconnected.
294
- global _disconnected_names;
308
+ # Intent is persisted in McpRegistry.disconnected_servers so it survives
309
+ # pod restarts and is visible across all pods.
295
310
  configs = _load_configs();
296
311
  if name not in configs {
297
312
  return {"error": f"Server '{name}' not found"};
298
313
  }
299
- _disconnected_names.add(name);
314
+ reg = _get_registry();
315
+ if name not in reg.disconnected_servers {
316
+ reg.disconnected_servers = list(reg.disconnected_servers) + [name];
317
+ }
300
318
  _submit(_close_connection(name));
301
319
  kv_delete(_TOOLS_CACHE_KEY);
302
- return {"status": "disconnected", "name": name};
320
+ return {"status": "disconnected", "name": name};
303
321
  }
304
322
 
305
323
 
306
324
  """Reconnect a previously-disconnected MCP server."""
307
325
  impl mcp_reconnect_server(name: str) -> dict {
308
- global _disconnected_names;
309
326
  configs = _load_configs();
310
327
  if name not in configs {
311
328
  return {"error": f"Server '{name}' not found"};
312
329
  }
313
- _disconnected_names.discard(name);
314
330
  # Close any stale connection first, then reconnect via list_tools.
315
331
  # Iterate configs to get a typed config value (subscript on untyped dict
316
332
  # produces Unknown in the Jac type checker — the for pattern is used
@@ -321,9 +337,14 @@ impl mcp_reconnect_server(name: str) -> dict {
321
337
  if n == name {
322
338
  try {
323
339
  tools = _submit(_async_list_tools(n, config));
340
+ # Only remove from disconnected_servers on success — keeps state
341
+ # consistent if connection fails (server stays disconnected).
342
+ reg = _get_registry();
343
+ if name in reg.disconnected_servers {
344
+ reg.disconnected_servers = [s for s in reg.disconnected_servers if s != name];
345
+ }
324
346
  return {"status": "connected", "name": name};
325
347
  } except Exception as e {
326
- _disconnected_names.add(name);
327
348
  return {"error": f"Failed to reconnect '{name}': {e}"};
328
349
  }
329
350
  }
@@ -334,15 +355,18 @@ impl mcp_reconnect_server(name: str) -> dict {
334
355
 
335
356
  impl mcp_delete_server(name: str) -> dict {
336
357
  # Permanently remove a user-added server from the registry.
337
- # Built-in servers cannot be deleted.
338
- if name in _builtin_names {
339
- return {"error": f"Built-in server '{name}' cannot be deleted"};
340
- }
358
+ # Built-in servers cannot be deleted — the builtin flag is stored as
359
+ # config["builtin"]=True inside McpRegistry.servers (graph-persisted),
341
360
  configs = _load_configs();
342
361
  if name not in configs {
343
362
  return {"error": f"Server '{name}' not found"};
344
363
  }
345
- _submit(_close_connection(name));
364
+ for (n, cfg) in configs.items() {
365
+ if n == name and cfg.get("builtin", False) {
366
+ return {"error": f"Built-in server '{name}' cannot be deleted"};
367
+ }
368
+ }
369
+ _submit(_close_connection(name));
346
370
  configs.pop(name);
347
371
  _save_configs(configs);
348
372
  kv_delete(_TOOLS_CACHE_KEY);
@@ -353,6 +377,8 @@ impl mcp_delete_server(name: str) -> dict {
353
377
  """Return all registered servers with their connection status and tool counts."""
354
378
  impl mcp_list_servers() -> list {
355
379
  configs = _load_configs();
380
+ # Read disconnected intent from the graph — persisted, cross-pod consistent.
381
+ disconnected = _get_registry().disconnected_servers;
356
382
  result: list = [];
357
383
  for (name, config) in configs.items() {
358
384
  entry: dict = {
@@ -363,7 +389,7 @@ impl mcp_list_servers() -> list {
363
389
  "latest_version": _jac_mcp_update_info.get("latest", "") if name == "jac-mcp" else ""
364
390
  };
365
391
  # Skip auto-connect for servers the user intentionally disconnected
366
- if name in _disconnected_names {
392
+ if name in disconnected {
367
393
  entry["status"] = "disconnected";
368
394
  entry["tool_count"] = 0;
369
395
  entry["tools"] = [];
@@ -388,16 +414,16 @@ impl mcp_list_servers() -> list {
388
414
 
389
415
  """Save a built-in server to the registry without a connectivity check."""
390
416
  impl mcp_register_builtin(name: str, config: dict) -> None {
391
- global _builtin_names;
392
- _builtin_names.add(name);
393
417
  configs = _load_configs();
394
418
  # Always update builtin config — the binary path can change between installs
395
419
  # (e.g. pipx venv vs workspace venv). Stale paths cause connection errors.
420
+ # builtin=True is stored in the config dict inside McpRegistry.servers so
421
+ # it is graph-persisted and cross-pod consistent — no RAM set needed.
396
422
  config["builtin"] = True;
397
423
  configs[name] = config;
398
424
  _save_configs(configs);
399
425
  kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — server set changed
400
- if name == "jac-mcp" {
426
+ if name == "jac-mcp" and not _service_mode_active() {
401
427
  threading.Thread(target=_check_jac_mcp_version, daemon=True).start();
402
428
  }
403
429
  }
@@ -411,8 +437,14 @@ impl mcp_get_tools() -> list {
411
437
  }
412
438
 
413
439
  configs = _load_configs();
440
+ # Read disconnected intent from the graph so tools from user-disconnected
441
+ # servers are never surfaced to the agent (bug fix: previously ignored).
442
+ disconnected = _get_registry().disconnected_servers;
414
443
  all_tools: list = [];
415
444
  for (name, config) in configs.items() {
445
+ if name in disconnected {
446
+ continue; # Respect user's disconnect intent — skip this server
447
+ }
416
448
  try {
417
449
  all_tools.extend(_submit(_async_list_tools(name, config)));
418
450
  } except Exception as e {
@@ -43,3 +43,10 @@ def mcp_call_tool(server_name: str, tool_name: str, arguments: dict) -> str;
43
43
  Saves now and tries it on first real tool call.
44
44
  """
45
45
  def mcp_register_builtin(name: str, config: dict) -> None;
46
+
47
+ """Return True when this process is serving multiple end-users (web/IDE).
48
+ Used to gate per-process best-effort tasks that only make sense in CLI."""
49
+ def _service_mode_active() -> bool;
50
+
51
+ """Background thread: check PyPI for latest jac-mcp version. No-op in service mode."""
52
+ def _check_jac_mcp_version() -> None;
@@ -206,6 +206,12 @@ impl reset() -> None {
206
206
 
207
207
 
208
208
  impl save_to_file(filepath: str = "") -> str {
209
+ # In web/IDE mode many users share the same server, so writing one user's
210
+ # cost to a local file would overwrite another user's data. Skip silently.
211
+ # In CLI mode the file is safe and useful.
212
+ if os.environ.get("JACCODER_WEB_MODE", "") or os.environ.get("JACCODER_IDE_MODE", "") {
213
+ return "";
214
+ }
209
215
  if not filepath {
210
216
  filepath = "jaccoder_cost.json";
211
217
  }