pal-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pal/__init__.py +29 -0
- pal/__main__.py +3 -0
- pal/access_manager/__init__.py +16 -0
- pal/access_manager/components/__init__.py +14 -0
- pal/access_manager/components/access_control.py +41 -0
- pal/access_manager/components/base.py +39 -0
- pal/access_manager/components/intervention.py +31 -0
- pal/access_manager/components/privilege_registry.py +30 -0
- pal/access_manager/pipeline.py +68 -0
- pal/access_manager/utils/__init__.py +2 -0
- pal/agent_constraints/__init__.py +3 -0
- pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
- pal/agent_constraints/components/__init__.py +3 -0
- pal/agent_constraints/components/constraints.py +22 -0
- pal/agent_constraints/pipeline.py +22 -0
- pal/agent_constraints/utils/__init__.py +2 -0
- pal/agents/__init__.py +12 -0
- pal/agents/coding.py +14 -0
- pal/agents/docs.py +15 -0
- pal/chat/__init__.py +4 -0
- pal/chat/demo.py +231 -0
- pal/chat/providers.py +23 -0
- pal/chat/service.py +591 -0
- pal/chat/services/__init__.py +26 -0
- pal/chat/services/base.py +142 -0
- pal/chat/services/data.py +142 -0
- pal/chat/services/media.py +123 -0
- pal/chat/services/web.py +60 -0
- pal/chat/sessions.py +108 -0
- pal/chat/tools.py +446 -0
- pal/chat/uploads.py +81 -0
- pal/cli.py +502 -0
- pal/compression/__init__.py +12 -0
- pal/compression/components/__init__.py +64 -0
- pal/compression/components/adverbs.py +20 -0
- pal/compression/components/hedges.py +54 -0
- pal/compression/components/interjections.py +20 -0
- pal/compression/components/minifier.py +56 -0
- pal/compression/components/output_compressor.py +13 -0
- pal/compression/components/pleonasms.py +54 -0
- pal/compression/components/punctuations.py +36 -0
- pal/compression/components/stopwords.py +62 -0
- pal/compression/history.py +346 -0
- pal/compression/pipeline.py +58 -0
- pal/compression/utils/__init__.py +30 -0
- pal/compression/utils/negations.py +18 -0
- pal/compression/utils/nltk_setup.py +20 -0
- pal/compression/utils/pronouns.py +27 -0
- pal/compression/utils/proselint_setup.py +9 -0
- pal/compression/utils/spacy_setup.py +22 -0
- pal/config/default.yaml +163 -0
- pal/config.py +504 -0
- pal/context_manager/__init__.py +7 -0
- pal/context_manager/chat_context.py +117 -0
- pal/context_manager/components/__init__.py +5 -0
- pal/context_manager/components/assembler.py +75 -0
- pal/context_manager/components/context_cache.py +45 -0
- pal/context_manager/components/kv_bridge.py +16 -0
- pal/context_manager/components/pager.py +48 -0
- pal/context_manager/components/simple_context_manager.py +56 -0
- pal/context_manager/components/turns.py +88 -0
- pal/context_manager/pipeline.py +27 -0
- pal/context_manager/utils/__init__.py +18 -0
- pal/context_manager/utils/hashing.py +5 -0
- pal/context_manager/utils/relevance.py +59 -0
- pal/context_manager/utils/storage.py +37 -0
- pal/documents/__init__.py +4 -0
- pal/documents/components/__init__.py +0 -0
- pal/documents/components/chunker.py +79 -0
- pal/documents/components/index.py +155 -0
- pal/documents/utils/__init__.py +1 -0
- pal/errors.py +40 -0
- pal/input_converters/__init__.py +8 -0
- pal/input_converters/components/__init__.py +0 -0
- pal/input_converters/components/base.py +38 -0
- pal/input_converters/components/pandoc_converter.py +87 -0
- pal/input_converters/components/pdf_converter.py +46 -0
- pal/input_converters/components/pptx_converter.py +52 -0
- pal/input_converters/components/xlsx_converter.py +38 -0
- pal/input_converters/pipeline.py +72 -0
- pal/input_converters/utils/__init__.py +1 -0
- pal/input_converters/workspace.py +56 -0
- pal/input_savers/INPUT_RULES.md +2 -0
- pal/input_savers/__init__.py +5 -0
- pal/input_savers/components/__init__.py +0 -0
- pal/input_savers/components/base.py +40 -0
- pal/input_savers/components/methods.py +41 -0
- pal/input_savers/components/verifier.py +39 -0
- pal/input_savers/pipeline.py +154 -0
- pal/input_savers/utils/__init__.py +0 -0
- pal/input_savers/utils/guards.py +94 -0
- pal/knowledge_graph/__init__.py +10 -0
- pal/knowledge_graph/components/__init__.py +0 -0
- pal/knowledge_graph/components/base.py +50 -0
- pal/knowledge_graph/components/builder.py +54 -0
- pal/knowledge_graph/components/retriever.py +100 -0
- pal/knowledge_graph/components/workspace.py +116 -0
- pal/knowledge_graph/pipeline.py +215 -0
- pal/knowledge_graph/utils/__init__.py +1 -0
- pal/latency_tracker/__init__.py +3 -0
- pal/latency_tracker/pipeline.py +86 -0
- pal/llm_scheduler/__init__.py +11 -0
- pal/llm_scheduler/api.py +137 -0
- pal/llm_scheduler/code.py +28 -0
- pal/llm_scheduler/core.py +92 -0
- pal/llm_scheduler/local.py +215 -0
- pal/llm_scheduler/pipeline.py +42 -0
- pal/llm_scheduler/types.py +92 -0
- pal/llm_wrapper/__init__.py +34 -0
- pal/llm_wrapper/adapter.py +316 -0
- pal/llm_wrapper/components/__init__.py +23 -0
- pal/llm_wrapper/components/adapter_types.py +90 -0
- pal/llm_wrapper/components/base.py +195 -0
- pal/llm_wrapper/components/features.py +104 -0
- pal/llm_wrapper/components/litellm_wrapper.py +215 -0
- pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
- pal/llm_wrapper/modes.py +73 -0
- pal/llm_wrapper/pipeline.py +61 -0
- pal/llm_wrapper/utils/__init__.py +4 -0
- pal/llm_wrapper/utils/registry.py +49 -0
- pal/llm_wrapper/utils/token_counting.py +10 -0
- pal/local_llm/__init__.py +5 -0
- pal/local_llm/components/__init__.py +0 -0
- pal/local_llm/components/huggingface_local.py +217 -0
- pal/local_llm/components/local_llm.py +60 -0
- pal/logging_setup.py +18 -0
- pal/memory/__init__.py +7 -0
- pal/memory/components/__init__.py +6 -0
- pal/memory/components/lru_k.py +56 -0
- pal/memory/components/manager.py +98 -0
- pal/memory/components/note.py +54 -0
- pal/memory/components/storage_manager.py +60 -0
- pal/memory/pipeline.py +122 -0
- pal/memory/utils/__init__.py +3 -0
- pal/memory/utils/keyword_overlap.py +3 -0
- pal/memory_tracker/__init__.py +3 -0
- pal/memory_tracker/components/__init__.py +0 -0
- pal/memory_tracker/components/process.py +56 -0
- pal/memory_tracker/pipeline.py +44 -0
- pal/metrics/__init__.py +4 -0
- pal/metrics/components/__init__.py +4 -0
- pal/metrics/components/collector.py +140 -0
- pal/metrics/components/exporter.py +28 -0
- pal/metrics/pipeline.py +49 -0
- pal/metrics/utils/__init__.py +2 -0
- pal/notes_manager/__init__.py +5 -0
- pal/notes_manager/components/__init__.py +0 -0
- pal/notes_manager/components/privacy.py +14 -0
- pal/notes_manager/components/recall.py +31 -0
- pal/notes_manager/components/tidy.py +28 -0
- pal/notes_manager/pipeline.py +138 -0
- pal/orchestrator/__init__.py +26 -0
- pal/orchestrator/components/__init__.py +26 -0
- pal/orchestrator/components/agent.py +105 -0
- pal/orchestrator/components/cgroup_manager.py +209 -0
- pal/orchestrator/components/context_interrupt.py +220 -0
- pal/orchestrator/components/limits.py +9 -0
- pal/orchestrator/components/scheduler.py +69 -0
- pal/orchestrator/components/syscall_scheduler.py +200 -0
- pal/orchestrator/pipeline.py +228 -0
- pal/orchestrator/utils/__init__.py +4 -0
- pal/orchestrator/utils/device.py +164 -0
- pal/orchestrator/utils/protocol.py +32 -0
- pal/os_kernel/__init__.py +5 -0
- pal/os_kernel/filesystem.py +80 -0
- pal/os_kernel/network.py +146 -0
- pal/os_kernel/pipeline.py +175 -0
- pal/os_kernel/sysinfo.py +36 -0
- pal/paths.py +59 -0
- pal/pipeline.py +177 -0
- pal/py.typed +0 -0
- pal/redact.py +19 -0
- pal/sdk/__init__.py +33 -0
- pal/sdk/adapters.py +43 -0
- pal/sdk/agent.py +99 -0
- pal/sdk/components/__init__.py +14 -0
- pal/sdk/components/autogen_adapter.py +65 -0
- pal/sdk/components/autogen_client.py +98 -0
- pal/sdk/components/framework_adapter_base.py +69 -0
- pal/sdk/components/metagpt_adapter.py +46 -0
- pal/sdk/components/open_interpreter_adapter.py +47 -0
- pal/sdk/components/tool_catalog.py +44 -0
- pal/sdk/os_api.py +84 -0
- pal/sdk/patterns.py +129 -0
- pal/sdk/pipeline.py +240 -0
- pal/sdk/queries.py +68 -0
- pal/sdk/savers.py +88 -0
- pal/sdk/utils/__init__.py +2 -0
- pal/storage_manager/__init__.py +17 -0
- pal/storage_manager/components/__init__.py +19 -0
- pal/storage_manager/components/base.py +50 -0
- pal/storage_manager/components/local_file_backend.py +84 -0
- pal/storage_manager/components/storage_queue.py +91 -0
- pal/storage_manager/components/vector_store_backend.py +122 -0
- pal/storage_manager/pipeline.py +153 -0
- pal/storage_manager/utils/__init__.py +4 -0
- pal/storage_manager/utils/memory_adapter.py +60 -0
- pal/storage_manager/utils/sdk.py +93 -0
- pal/studio/__init__.py +0 -0
- pal/studio/builder.py +166 -0
- pal/studio/catalog.py +90 -0
- pal/studio/checks.py +129 -0
- pal/studio/clients.py +6 -0
- pal/studio/editor.py +137 -0
- pal/studio/files.py +6 -0
- pal/studio/models.py +6 -0
- pal/studio/plan.py +149 -0
- pal/studio/planner.py +205 -0
- pal/studio/policy.py +69 -0
- pal/studio/routes.py +254 -0
- pal/studio/runs.py +210 -0
- pal/task_router/__init__.py +11 -0
- pal/task_router/components/__init__.py +0 -0
- pal/task_router/components/answerer.py +37 -0
- pal/task_router/components/base.py +34 -0
- pal/task_router/components/classifier.py +40 -0
- pal/task_router/components/gates.py +23 -0
- pal/task_router/pipeline.py +61 -0
- pal/task_router/utils/__init__.py +1 -0
- pal/terminal/__init__.py +1 -0
- pal/terminal/backend.py +141 -0
- pal/terminal/bench.py +173 -0
- pal/terminal/cli.py +826 -0
- pal/terminal/clients.py +170 -0
- pal/terminal/commands.py +401 -0
- pal/terminal/decide.py +242 -0
- pal/terminal/files.py +55 -0
- pal/terminal/flow_cli.py +439 -0
- pal/terminal/hardware.py +199 -0
- pal/terminal/history.py +88 -0
- pal/terminal/keys.py +83 -0
- pal/terminal/local_models.py +261 -0
- pal/terminal/logo.py +117 -0
- pal/terminal/models.py +99 -0
- pal/terminal/policy.py +54 -0
- pal/terminal/project.py +413 -0
- pal/terminal/review.py +65 -0
- pal/terminal/security.py +239 -0
- pal/terminal/session.py +503 -0
- pal/terminal/setup.py +354 -0
- pal/terminal/syntax.py +98 -0
- pal/terminal/theme.py +37 -0
- pal/terminal/ui.py +647 -0
- pal/terminal/voice.py +137 -0
- pal/terminal/workflow.py +624 -0
- pal/token_tracker/__init__.py +3 -0
- pal/token_tracker/pipeline.py +54 -0
- pal/tokens.py +21 -0
- pal/tool_catalog.py +107 -0
- pal/tool_manager/__init__.py +17 -0
- pal/tool_manager/builtin.py +82 -0
- pal/tool_manager/components/__init__.py +19 -0
- pal/tool_manager/components/base.py +53 -0
- pal/tool_manager/components/conflict_resolver.py +27 -0
- pal/tool_manager/components/param_validator.py +41 -0
- pal/tool_manager/components/tool_call_queue.py +111 -0
- pal/tool_manager/components/tool_registry.py +60 -0
- pal/tool_manager/pipeline.py +80 -0
- pal/tool_manager/utils/__init__.py +3 -0
- pal/web/__init__.py +3 -0
- pal/web/app.py +276 -0
- pal/web/static/editor.js +219 -0
- pal/web/static/fonts/LICENSE-Inter.txt +3 -0
- pal/web/static/fonts/inter-400.woff2 +0 -0
- pal/web/static/fonts/inter-500.woff2 +0 -0
- pal/web/static/fonts/inter-600.woff2 +0 -0
- pal/web/static/fonts/inter-700.woff2 +0 -0
- pal/web/static/inter.css +5 -0
- pal/web/static/pal-logo-256.png +0 -0
- pal/web/static/pal-logo-32.png +0 -0
- pal/web/static/pal-logo-64.png +0 -0
- pal/web/static/studio.css +306 -0
- pal/web/static/studio.js +533 -0
- pal/web/static/voice.js +76 -0
- pal/web/templates/index.html +368 -0
- pal/web/templates/studio.html +120 -0
- pal_code-0.1.0.dist-info/METADATA +379 -0
- pal_code-0.1.0.dist-info/RECORD +282 -0
- pal_code-0.1.0.dist-info/WHEEL +5 -0
- pal_code-0.1.0.dist-info/entry_points.txt +2 -0
- pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- pal_code-0.1.0.dist-info/top_level.txt +1 -0
pal/web/app.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""HTTP layer: a thin Flask app around ChatService. No business logic lives here.
|
|
2
|
+
|
|
3
|
+
- create_app() is a factory, so tests and servers build their own app around their own service.
|
|
4
|
+
- Errors: PalError subclasses map to their HTTP status with a safe message; anything else is
|
|
5
|
+
logged with a traceback and answered with a generic 500 (no traceback ever reaches the client).
|
|
6
|
+
- API keys arrive per request and are passed straight through; they are never logged or stored.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import uuid
|
|
15
|
+
from dataclasses import asdict
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from flask import Flask, Response, jsonify, render_template, request
|
|
19
|
+
from werkzeug.exceptions import HTTPException
|
|
20
|
+
|
|
21
|
+
from pal.chat import API_PROVIDERS, ChatService
|
|
22
|
+
from pal.chat.demo import Demo
|
|
23
|
+
from pal.chat.sessions import validate_session_id
|
|
24
|
+
from pal.chat.uploads import ( # noqa: F401 (read_upload is part of this module's API)
|
|
25
|
+
MEDIA_TYPES,
|
|
26
|
+
_clean_filename,
|
|
27
|
+
_is_text,
|
|
28
|
+
read_upload,
|
|
29
|
+
)
|
|
30
|
+
from pal.config import Settings
|
|
31
|
+
from pal.errors import NotFoundError, PalError, UserInputError
|
|
32
|
+
from pal.input_savers.components.methods import METHODS
|
|
33
|
+
from pal.llm_wrapper.modes import default_mode, describe_modes
|
|
34
|
+
from pal.sdk.agent import list_agents
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
_SECURITY_HEADERS = {
|
|
39
|
+
"X-Content-Type-Options": "nosniff",
|
|
40
|
+
"X-Frame-Options": "DENY",
|
|
41
|
+
"Referrer-Policy": "no-referrer",
|
|
42
|
+
"Cache-Control": "no-store",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _saver_json(res) -> Optional[dict]:
|
|
47
|
+
if res is None: # turn handled without an API call: no input saver ran
|
|
48
|
+
return None
|
|
49
|
+
return {
|
|
50
|
+
"original": res.original, "reduced": res.reduced,
|
|
51
|
+
"tokens_original": res.tokens_original, "tokens_reduced": res.tokens_reduced,
|
|
52
|
+
"tokens_saved": res.tokens_saved,
|
|
53
|
+
"steps": [asdict(s) for s in res.steps],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _flag(v: Any) -> bool:
|
|
58
|
+
return v is True or str(v).lower() in ("1", "true", "on", "yes")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _pasted(v: Any) -> list:
|
|
62
|
+
"""Pasted spans the client tracked; the LLM never sees or edits them."""
|
|
63
|
+
if isinstance(v, list):
|
|
64
|
+
return [x for x in v if isinstance(x, str)]
|
|
65
|
+
try:
|
|
66
|
+
out = json.loads(v or "[]")
|
|
67
|
+
except ValueError:
|
|
68
|
+
return []
|
|
69
|
+
return [x for x in out if isinstance(x, str)] if isinstance(out, list) else []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_app(service: Optional[ChatService] = None, settings: Optional[Settings] = None, studio: Optional[dict] = None) -> Flask:
|
|
73
|
+
"""`studio` passes options to the Studio (its planner model, builder model and model discovery); tests use it to plug in fakes."""
|
|
74
|
+
service = service or ChatService(settings)
|
|
75
|
+
cfg = service.settings
|
|
76
|
+
app = Flask(__name__)
|
|
77
|
+
app.config["MAX_CONTENT_LENGTH"] = cfg.server.max_upload_mb * 1024 * 1024
|
|
78
|
+
app.extensions["pal_service"] = service
|
|
79
|
+
demo = Demo(service)
|
|
80
|
+
|
|
81
|
+
@app.after_request
|
|
82
|
+
def _headers(resp: Response) -> Response:
|
|
83
|
+
for k, v in _SECURITY_HEADERS.items():
|
|
84
|
+
resp.headers.setdefault(k, v)
|
|
85
|
+
return resp
|
|
86
|
+
|
|
87
|
+
@app.errorhandler(PalError)
|
|
88
|
+
def _pal_error(e: PalError):
|
|
89
|
+
if e.http_status >= 500:
|
|
90
|
+
log.error("request failed: %s: %s", type(e).__name__, e)
|
|
91
|
+
msg = str(e) if e.public else "internal error"
|
|
92
|
+
return jsonify({"error": msg, "code": type(e).__name__}), e.http_status
|
|
93
|
+
|
|
94
|
+
@app.errorhandler(HTTPException)
|
|
95
|
+
def _http_error(e: HTTPException):
|
|
96
|
+
msg = "upload too large" if e.code == 413 else (e.description or e.name)
|
|
97
|
+
return jsonify({"error": msg, "code": e.name.replace(" ", "")}), e.code or 500
|
|
98
|
+
|
|
99
|
+
@app.errorhandler(Exception)
|
|
100
|
+
def _unexpected(e: Exception):
|
|
101
|
+
log.exception("unhandled error")
|
|
102
|
+
return jsonify({"error": "internal error", "code": "InternalError"}), 500
|
|
103
|
+
|
|
104
|
+
@app.get("/")
|
|
105
|
+
def index():
|
|
106
|
+
return render_template("index.html")
|
|
107
|
+
|
|
108
|
+
@app.get("/healthz")
|
|
109
|
+
def healthz():
|
|
110
|
+
return jsonify({"ok": True})
|
|
111
|
+
|
|
112
|
+
@app.get("/api/config")
|
|
113
|
+
def config():
|
|
114
|
+
return jsonify({
|
|
115
|
+
"providers": {p: bool(os.environ.get(env)) for p, env in API_PROVIDERS.items()},
|
|
116
|
+
"llm_modes": describe_modes(), "llm_mode": default_mode(),
|
|
117
|
+
"methods": [{"id": m, "name": METHODS[m].name} for m in cfg.input_methods],
|
|
118
|
+
"local_llm": {k: v for k, v in cfg.local_llm.items() if k != "api_key"},
|
|
119
|
+
"mode": cfg.input_mode,
|
|
120
|
+
"routing": {"enabled": cfg.task_routing.enabled, "default": cfg.task_routing.default},
|
|
121
|
+
"graph": {"enabled": service.graph.enabled, "available": service.graph.available()},
|
|
122
|
+
"memory": {"enabled": cfg.memory.enabled},
|
|
123
|
+
"agents": list_agents(service.agents) if cfg.agents.enabled else [],
|
|
124
|
+
"metrics": {"enabled": cfg.metrics.enabled},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
@app.get("/api/llm/modes")
|
|
128
|
+
def llm_modes():
|
|
129
|
+
return jsonify({"default": default_mode(), "modes": describe_modes()})
|
|
130
|
+
|
|
131
|
+
@app.get("/api/llm/models")
|
|
132
|
+
def llm_models():
|
|
133
|
+
"""OpenRouter's models for the Explore mode: ?q=words&free=1&limit=50."""
|
|
134
|
+
try:
|
|
135
|
+
limit = max(1, min(int(request.args.get("limit", "50")), 500))
|
|
136
|
+
except ValueError:
|
|
137
|
+
raise UserInputError("limit must be a number") from None
|
|
138
|
+
free = request.args.get("free", "").lower() in ("1", "true", "yes")
|
|
139
|
+
return jsonify({"models": service.model_catalogue(request.args.get("q", ""), free, limit)})
|
|
140
|
+
|
|
141
|
+
@app.get("/api/voice")
|
|
142
|
+
def voice_status():
|
|
143
|
+
"""Which speech-to-text engines this server has: the page records and sends the audio here when there is one."""
|
|
144
|
+
from pal.terminal import voice
|
|
145
|
+
return jsonify({"engines": voice.engines()})
|
|
146
|
+
|
|
147
|
+
@app.post("/api/transcribe")
|
|
148
|
+
def transcribe():
|
|
149
|
+
"""The recorded audio (the request body, as the browser's MediaRecorder made it) -> text. Nothing is kept."""
|
|
150
|
+
from pal.terminal import voice
|
|
151
|
+
audio = request.get_data(cache=False)
|
|
152
|
+
if not audio:
|
|
153
|
+
raise UserInputError("no audio received")
|
|
154
|
+
kind = (request.content_type or "").split(";")[0]
|
|
155
|
+
suffix = {"audio/webm": ".webm", "audio/ogg": ".ogg", "audio/mp4": ".m4a", "audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/x-wav": ".wav"}.get(kind, ".webm")
|
|
156
|
+
try:
|
|
157
|
+
heard = voice.transcribe(audio, suffix)
|
|
158
|
+
except voice.VoiceUnavailable as e:
|
|
159
|
+
raise UserInputError(str(e)) from None
|
|
160
|
+
return jsonify({"text": heard.text, "engine": heard.engine})
|
|
161
|
+
|
|
162
|
+
@app.get("/api/local_status")
|
|
163
|
+
def local_status():
|
|
164
|
+
return jsonify(service.local_status())
|
|
165
|
+
|
|
166
|
+
@app.post("/api/chat")
|
|
167
|
+
def chat():
|
|
168
|
+
d: dict[str, Any]
|
|
169
|
+
if request.is_json:
|
|
170
|
+
d, files = request.get_json(silent=True) or {}, []
|
|
171
|
+
else:
|
|
172
|
+
d = dict(request.form.to_dict())
|
|
173
|
+
if "methods" in request.form:
|
|
174
|
+
d["methods"] = request.form.getlist("methods")
|
|
175
|
+
files = request.files.getlist("files")
|
|
176
|
+
sid = d.get("session_id") or uuid.uuid4().hex
|
|
177
|
+
attachments, errors = [], []
|
|
178
|
+
for f in files:
|
|
179
|
+
try:
|
|
180
|
+
attachments.append(read_upload(service, f, sid))
|
|
181
|
+
except UserInputError as e:
|
|
182
|
+
errors.append(str(e))
|
|
183
|
+
message = d.get("message") or ""
|
|
184
|
+
if errors and not (message.strip() or attachments):
|
|
185
|
+
return jsonify({"error": "; ".join(errors)}), 400
|
|
186
|
+
res = service.send(
|
|
187
|
+
sid, d.get("provider", ""), d.get("model", ""), d.get("api_key") or None, message, attachments,
|
|
188
|
+
d.get("methods"), d.get("mode") or None, _pasted(d.get("pasted")), d.get("route") or None,
|
|
189
|
+
agent=d.get("agent") or None)
|
|
190
|
+
return jsonify({
|
|
191
|
+
"reply": res.reply, "sent": res.sent,
|
|
192
|
+
"input_tokens": res.input_tokens, "output_tokens": res.output_tokens,
|
|
193
|
+
"saver": _saver_json(res.saver), "file_errors": errors,
|
|
194
|
+
"handled_by": res.handled_by, "route_reason": res.route_reason,
|
|
195
|
+
"graph": res.graph, "documents": res.documents, "memory": res.memory,
|
|
196
|
+
"context": res.context, "tools": res.tools, "latency": res.latency, "fallback": res.fallback,
|
|
197
|
+
"cost_usd": res.cost_usd, "files": res.files,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
@app.post("/api/experiment")
|
|
201
|
+
def experiment():
|
|
202
|
+
d = request.get_json(silent=True) or {}
|
|
203
|
+
if not (d.get("text") or "").strip():
|
|
204
|
+
raise UserInputError("enter some text")
|
|
205
|
+
out = service.experiment(
|
|
206
|
+
d["text"], d.get("methods"), d.get("provider", ""), d.get("model", ""), d.get("api_key") or None,
|
|
207
|
+
_flag(d.get("call_api")), d.get("mode") or None, _pasted(d.get("pasted")))
|
|
208
|
+
return jsonify({"saver": _saver_json(out["saver"]), "counter": out["counter"], "api": out["api"]})
|
|
209
|
+
|
|
210
|
+
@app.post("/api/reset")
|
|
211
|
+
def reset():
|
|
212
|
+
service.reset((request.get_json(silent=True) or {}).get("session_id", ""))
|
|
213
|
+
return jsonify({"ok": True})
|
|
214
|
+
|
|
215
|
+
@app.get("/api/workspace")
|
|
216
|
+
def workspace_list():
|
|
217
|
+
"""Files an agent saved for this session (its workspace in the OS kernel interface)."""
|
|
218
|
+
sess = _session_or_404(request.args.get("session_id", ""))
|
|
219
|
+
return jsonify({"files": service.os_kernel.workspace(sess.agent_id).list()})
|
|
220
|
+
|
|
221
|
+
@app.get("/api/workspace/file")
|
|
222
|
+
def workspace_file():
|
|
223
|
+
sess = _session_or_404(request.args.get("session_id", ""))
|
|
224
|
+
path = request.args.get("path", "")
|
|
225
|
+
data = service.os_kernel.workspace(sess.agent_id).read_bytes(path) # validated: inside this session's workspace only
|
|
226
|
+
ext = "." + path.rsplit(".", 1)[1].lower() if "." in path else ""
|
|
227
|
+
mime = MEDIA_TYPES.get(ext, "text/plain" if _is_text(data) else "application/octet-stream")
|
|
228
|
+
name = re.sub(r"[^A-Za-z0-9._-]", "_", path.rsplit("/", 1)[-1]) or "file.txt"
|
|
229
|
+
return Response(data, mimetype=mime, headers={"Content-Disposition": f'attachment; filename="{name}"'})
|
|
230
|
+
|
|
231
|
+
def _session_or_404(session_id: str):
|
|
232
|
+
validate_session_id(session_id) # a malformed id is a 400
|
|
233
|
+
sess = service.sessions.get(session_id)
|
|
234
|
+
if sess is None:
|
|
235
|
+
raise NotFoundError("no such session")
|
|
236
|
+
return sess
|
|
237
|
+
|
|
238
|
+
@app.get("/api/metrics")
|
|
239
|
+
def metrics():
|
|
240
|
+
sid = request.args.get("session") or None
|
|
241
|
+
return jsonify({"summary": service.metrics.chat_summary(sid), "recent": service.metrics.turns()[-30:],
|
|
242
|
+
"scheduler": service.scheduler.snapshot(), "memory": service.metrics.memory.snapshot()})
|
|
243
|
+
|
|
244
|
+
@app.post("/api/metrics/reset")
|
|
245
|
+
def metrics_reset():
|
|
246
|
+
service.metrics.reset()
|
|
247
|
+
return jsonify({"ok": True})
|
|
248
|
+
|
|
249
|
+
@app.post("/api/demo/start")
|
|
250
|
+
def demo_start():
|
|
251
|
+
d = request.get_json(silent=True) or {}
|
|
252
|
+
return jsonify(demo.start(d.get("provider", ""), d.get("model", ""), d.get("api_key") or None))
|
|
253
|
+
|
|
254
|
+
@app.post("/api/demo/step")
|
|
255
|
+
def demo_step():
|
|
256
|
+
d = request.get_json(silent=True) or {}
|
|
257
|
+
return jsonify(demo.step(d.get("run_id", ""), d.get("api_key") or None))
|
|
258
|
+
|
|
259
|
+
@app.post("/api/demo/cancel")
|
|
260
|
+
def demo_cancel():
|
|
261
|
+
demo.cancel((request.get_json(silent=True) or {}).get("run_id", ""))
|
|
262
|
+
return jsonify({"ok": True})
|
|
263
|
+
|
|
264
|
+
from pal.studio.routes import register_studio
|
|
265
|
+
register_studio(app, service, **(studio or {})) # the Studio: describe a product, review the plan, watch it being built
|
|
266
|
+
|
|
267
|
+
@app.get("/api/memory")
|
|
268
|
+
def memory_status():
|
|
269
|
+
return jsonify({"enabled": cfg.memory.enabled, "notes": service.memory_count() if cfg.memory.enabled else 0,
|
|
270
|
+
"stats": service.notes.stats() if cfg.memory.enabled else None})
|
|
271
|
+
|
|
272
|
+
@app.delete("/api/memory")
|
|
273
|
+
def memory_clear():
|
|
274
|
+
return jsonify({"cleared": service.clear_memory() if cfg.memory.enabled else 0})
|
|
275
|
+
|
|
276
|
+
return app
|
pal/web/static/editor.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/* Pal editor: open a finished build as a project. File list, tabs, a code pane with line numbers, and "Ask Pal" on the right: Pal proposes a change as a diff,
|
|
2
|
+
you accept or reject it. Every request goes through the token-saving client on the server, and the panel shows what it cost.
|
|
3
|
+
Everything a model or a file contains is shown with textContent (or as a textarea's value), never as HTML. */
|
|
4
|
+
(() => {
|
|
5
|
+
'use strict';
|
|
6
|
+
const $ = (id) => document.getElementById(id);
|
|
7
|
+
const wait = () => new Promise((r) => { const t = setInterval(() => { if (window.studio && window.studio.util) { clearInterval(t); r(window.studio); } }, 20); });
|
|
8
|
+
|
|
9
|
+
wait().then((studio) => {
|
|
10
|
+
const { h, api, post, n } = studio.util;
|
|
11
|
+
const E = { run: null, arm: '', files: [], tabs: [], cur: '', buf: {}, proposal: null, busy: false, ctl: null, seq: 0 };
|
|
12
|
+
|
|
13
|
+
const has = () => !!E.run;
|
|
14
|
+
const dirty = (p) => E.buf[p] && E.buf[p].text !== E.buf[p].orig;
|
|
15
|
+
const say = (text, cls = '') => { const m = h('div', { class: 'edmsg ' + cls, text }); $('edmsgs').append(m); $('edmsgs').scrollTop = $('edmsgs').scrollHeight; return m; };
|
|
16
|
+
const status = (t, bad = false) => { const s = $('edstatus'); s.textContent = t; s.classList.toggle('bad', bad); };
|
|
17
|
+
const armQ = () => `arm=${encodeURIComponent(E.arm)}`;
|
|
18
|
+
|
|
19
|
+
function renderFiles() {
|
|
20
|
+
$('edfiles').replaceChildren(...E.files.map((f) => h('li', {}, h('button', { type: 'button', class: f.path === E.cur ? 'active' : '', 'data-file': f.path },
|
|
21
|
+
h('span', { class: 'fname', text: f.path }), dirty(f.path) ? h('span', { class: 'dotmark', text: '●', title: 'Unsaved changes' }) : null))));
|
|
22
|
+
}
|
|
23
|
+
function renderTabs() {
|
|
24
|
+
$('edtabs').replaceChildren(...E.tabs.map((p) => h('div', { class: 'edtab' + (p === E.cur ? ' active' : '') },
|
|
25
|
+
h('button', { type: 'button', class: 'edtabname', 'data-tab': p, text: p.split('/').pop() + (dirty(p) ? ' ●' : ''), title: p }),
|
|
26
|
+
h('button', { type: 'button', class: 'edtabx', 'data-close': p, 'aria-label': 'Close ' + p, text: '×' }))));
|
|
27
|
+
}
|
|
28
|
+
function gutter() {
|
|
29
|
+
const code = $('edcode'), lines = code.value.split('\n').length;
|
|
30
|
+
$('edgutter').textContent = Array.from({ length: lines }, (_, i) => i + 1).join('\n');
|
|
31
|
+
$('edgutter').scrollTop = code.scrollTop;
|
|
32
|
+
}
|
|
33
|
+
function caret() {
|
|
34
|
+
const code = $('edcode'), before = code.value.slice(0, code.selectionStart).split('\n');
|
|
35
|
+
$('edpos').textContent = `Ln ${before.length}, Col ${before[before.length - 1].length + 1}`;
|
|
36
|
+
}
|
|
37
|
+
function show() {
|
|
38
|
+
const p = E.cur, b = E.buf[p];
|
|
39
|
+
$('edcode').hidden = !p; $('edgutter').hidden = !p; $('edempty').hidden = !!p;
|
|
40
|
+
if (!p) { $('edpath').textContent = ''; $('edsave').disabled = true; renderFiles(); renderTabs(); return; }
|
|
41
|
+
$('edcode').value = b.text;
|
|
42
|
+
$('edcode').readOnly = !!b.readonly || E.busy;
|
|
43
|
+
$('edpath').textContent = p + (b.readonly ? ' (too large to edit here)' : '');
|
|
44
|
+
$('edsave').disabled = !dirty(p) || !!b.readonly;
|
|
45
|
+
gutter(); caret(); renderFiles(); renderTabs();
|
|
46
|
+
status(dirty(p) ? 'Unsaved changes' : 'Saved');
|
|
47
|
+
}
|
|
48
|
+
async function openFile(p) {
|
|
49
|
+
if (!E.buf[p]) {
|
|
50
|
+
const d = await api(`/api/studio/run/${E.run}/file?${armQ()}&path=${encodeURIComponent(p)}&full=1`);
|
|
51
|
+
E.buf[p] = { text: d.content, orig: d.content, readonly: d.truncated };
|
|
52
|
+
}
|
|
53
|
+
if (!E.tabs.includes(p)) E.tabs.push(p);
|
|
54
|
+
E.cur = p;
|
|
55
|
+
show();
|
|
56
|
+
}
|
|
57
|
+
function closeTab(p) {
|
|
58
|
+
if (dirty(p) && !window.confirm(`Close ${p} and lose the unsaved changes?`)) return;
|
|
59
|
+
const i = E.tabs.indexOf(p);
|
|
60
|
+
E.tabs.splice(i, 1);
|
|
61
|
+
delete E.buf[p];
|
|
62
|
+
if (E.cur === p) E.cur = E.tabs[Math.min(i, E.tabs.length - 1)] || '';
|
|
63
|
+
if (E.cur) openFile(E.cur); else show();
|
|
64
|
+
}
|
|
65
|
+
async function refreshFiles() {
|
|
66
|
+
const d = await api(`/api/studio/run/${E.run}/files?${armQ()}`);
|
|
67
|
+
E.files = d.files;
|
|
68
|
+
renderFiles();
|
|
69
|
+
}
|
|
70
|
+
async function save(p = E.cur) {
|
|
71
|
+
const b = E.buf[p];
|
|
72
|
+
if (!b || !dirty(p) || b.readonly) return true;
|
|
73
|
+
try {
|
|
74
|
+
const d = await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: p, content: b.text }) });
|
|
75
|
+
b.orig = b.text;
|
|
76
|
+
await refreshFiles();
|
|
77
|
+
status(d.saved_to_disk ? 'Saved' : 'Saved (this build was a dry run, so it lives in this session only)');
|
|
78
|
+
if (p === E.cur) { $('edsave').disabled = true; renderTabs(); }
|
|
79
|
+
return true;
|
|
80
|
+
} catch (e) { status('Could not save: ' + e.message, true); return false; }
|
|
81
|
+
}
|
|
82
|
+
async function saveAll() { for (const p of Object.keys(E.buf)) if (!(await save(p))) return false; return true; }
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------- Ask Pal
|
|
85
|
+
function meter(session) {
|
|
86
|
+
const m = $('edmeter');
|
|
87
|
+
if (!session || !session.asks) { m.textContent = 'Each request goes through the token-saving layer.'; return; }
|
|
88
|
+
m.replaceChildren(h('b', { text: n(session.pal_tokens) }), ` tokens used over ${session.asks} request${session.asks > 1 ? 's' : ''}. A plain chat that resends the project would use about `,
|
|
89
|
+
h('b', { text: n(session.plain_estimate) }), session.saved_pct > 0 ? ` (${session.saved_pct}% saved, estimated).` : '. Pal has not saved tokens yet: it pays off as the project and the conversation grow.');
|
|
90
|
+
}
|
|
91
|
+
function renderProposal(pr) {
|
|
92
|
+
E.proposal = pr;
|
|
93
|
+
const box = $('eddiff');
|
|
94
|
+
box.hidden = !pr;
|
|
95
|
+
$('edcode').hidden = !!pr || !E.cur; $('edgutter').hidden = !!pr || !E.cur; $('edempty').hidden = !!pr || !!E.cur;
|
|
96
|
+
if (!pr) { box.replaceChildren(); return; }
|
|
97
|
+
const parts = pr.files.map((f) => h('section', { class: 'dfile' },
|
|
98
|
+
h('div', { class: 'dhead' }, h('b', { text: f.path }), f.new ? ' (new file)' : ''),
|
|
99
|
+
h('pre', { class: 'dlines' }, ...f.diff.map(([t, x]) => h('span', { class: 'dl d-' + t, text: (t === 'add' ? '+ ' : t === 'del' ? '- ' : t === 'hunk' ? '' : ' ') + x + '\n' })))));
|
|
100
|
+
box.replaceChildren(h('div', { class: 'dbar' }, h('span', { text: `Pal suggests changing ${pr.files.length} file${pr.files.length > 1 ? 's' : ''}` }),
|
|
101
|
+
h('span', { class: 'sp' }), h('button', { type: 'button', class: 'btn', id: 'edreject', text: 'Reject' }), h('button', { type: 'button', class: 'btn primary', id: 'edaccept', text: 'Accept' })), ...parts);
|
|
102
|
+
}
|
|
103
|
+
async function accept() {
|
|
104
|
+
const pr = E.proposal;
|
|
105
|
+
if (!pr) return;
|
|
106
|
+
try {
|
|
107
|
+
for (const f of pr.files) {
|
|
108
|
+
await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: f.path, content: f.content }) });
|
|
109
|
+
if (E.buf[f.path]) E.buf[f.path] = { text: f.content, orig: f.content, readonly: false };
|
|
110
|
+
}
|
|
111
|
+
await refreshFiles();
|
|
112
|
+
say(`Applied to ${pr.files.map((f) => f.path).join(', ')}.`, 'ok');
|
|
113
|
+
renderProposal(null);
|
|
114
|
+
const first = pr.files[0].path;
|
|
115
|
+
if (!E.tabs.includes(first)) await openFile(first); else show();
|
|
116
|
+
} catch (e) { say('Could not apply the change: ' + e.message, 'bad'); }
|
|
117
|
+
}
|
|
118
|
+
function reject() { renderProposal(null); say('Rejected. Nothing changed.'); show(); }
|
|
119
|
+
function busy(b) { E.busy = b; $('edsend').disabled = b || !$('edask').value.trim(); $('edask').disabled = b; $('edcode').readOnly = b || !!(E.cur && E.buf[E.cur].readonly); $('edstop').hidden = !b; }
|
|
120
|
+
async function ask(text) {
|
|
121
|
+
if (E.busy || !text) return;
|
|
122
|
+
if (E.proposal) renderProposal(null);
|
|
123
|
+
say(text, 'me');
|
|
124
|
+
busy(true);
|
|
125
|
+
if (!(await saveAll())) { say('Fix the save problem first, then ask again.', 'bad'); busy(false); return; }
|
|
126
|
+
const pending = say('Pal is reading the project...', 'wait');
|
|
127
|
+
const mine = ++E.seq;
|
|
128
|
+
E.ctl = new AbortController();
|
|
129
|
+
try {
|
|
130
|
+
const d = await post(`/api/studio/run/${E.run}/ask`, { arm: E.arm, path: E.cur, instruction: text, api_key: studio.state.apiKey || undefined }, E.ctl.signal);
|
|
131
|
+
if (mine !== E.seq) return;
|
|
132
|
+
pending.remove();
|
|
133
|
+
if (d.message) say(d.message);
|
|
134
|
+
d.refused.forEach((r) => say(`Refused ${r.path}: ${r.reason}`, 'bad'));
|
|
135
|
+
if (d.note) say(d.note);
|
|
136
|
+
const u = d.usage;
|
|
137
|
+
say(`Read ${n(u.prompt_tokens)} tokens, wrote ${n(u.completion_tokens)}${u.helper_tokens ? `, helper ${n(u.helper_tokens)}` : ''} in ${u.seconds}s. A plain chat would have read about ${n(u.plain_estimate - u.completion_tokens)}.`, 'meta');
|
|
138
|
+
meter(d.session);
|
|
139
|
+
if (d.files.length) renderProposal(d); else if (!d.message) say('Pal did not suggest any change.');
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (mine !== E.seq) return;
|
|
142
|
+
pending.remove();
|
|
143
|
+
say(e.name === 'AbortError' ? 'Stopped.' : 'Something went wrong: ' + e.message, 'bad');
|
|
144
|
+
} finally { if (mine === E.seq) busy(false); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------- opening and closing the editor
|
|
148
|
+
function pickArm(R) {
|
|
149
|
+
const arms = Object.entries((R.summary || {}).arms || {}).filter(([, a]) => a.files > 0).map(([k]) => k);
|
|
150
|
+
return arms.includes('pal') ? 'pal' : arms[arms.length - 1] || '';
|
|
151
|
+
}
|
|
152
|
+
studio.editorArm = pickArm;
|
|
153
|
+
async function open(R) {
|
|
154
|
+
const arm = pickArm(R);
|
|
155
|
+
if (!arm) return;
|
|
156
|
+
if (E.run !== R.id || E.arm !== arm) {
|
|
157
|
+
Object.assign(E, { run: R.id, arm, files: [], tabs: [], cur: '', buf: {}, proposal: null });
|
|
158
|
+
$('edmsgs').replaceChildren();
|
|
159
|
+
say('Ask me to change the open file, add a feature, or explain the code. I will show you the change first, and nothing is saved until you accept it.');
|
|
160
|
+
meter(null);
|
|
161
|
+
renderProposal(null);
|
|
162
|
+
$('eddownload').href = `/api/studio/run/${R.id}/download?arm=${encodeURIComponent(arm)}`;
|
|
163
|
+
}
|
|
164
|
+
document.querySelector('.mainwrap').hidden = true;
|
|
165
|
+
$('editor').hidden = false;
|
|
166
|
+
$('viewchat').classList.remove('active'); $('vieweditor').classList.add('active');
|
|
167
|
+
$('side').classList.remove('open');
|
|
168
|
+
await refreshFiles();
|
|
169
|
+
const first = E.cur || (['index.html', 'main.py', 'readme.md'].map((w) => E.files.find((f) => f.path.toLowerCase() === w)).find(Boolean) || E.files[0] || {}).path || '';
|
|
170
|
+
if (first) await openFile(first); else show();
|
|
171
|
+
}
|
|
172
|
+
function close() {
|
|
173
|
+
$('editor').hidden = true;
|
|
174
|
+
document.querySelector('.mainwrap').hidden = false;
|
|
175
|
+
$('vieweditor').classList.remove('active'); $('viewchat').classList.add('active');
|
|
176
|
+
}
|
|
177
|
+
studio.openEditor = open;
|
|
178
|
+
|
|
179
|
+
$('viewchat').addEventListener('click', close);
|
|
180
|
+
$('vieweditor').addEventListener('click', () => { const R = studio.state.runs.slice().reverse().find((r) => pickArm(r)); if (R) open(R); });
|
|
181
|
+
$('edfiles').addEventListener('click', (e) => { const b = e.target.closest('button[data-file]'); if (b && !E.busy) openFile(b.dataset.file); });
|
|
182
|
+
$('edtabs').addEventListener('click', (e) => {
|
|
183
|
+
const c = e.target.closest('button[data-close]'), t = e.target.closest('button[data-tab]');
|
|
184
|
+
if (c) closeTab(c.dataset.close); else if (t) openFile(t.dataset.tab);
|
|
185
|
+
});
|
|
186
|
+
$('edcode').addEventListener('input', () => { const b = E.buf[E.cur]; if (!b) return; b.text = $('edcode').value; $('edsave').disabled = !dirty(E.cur); status(dirty(E.cur) ? 'Unsaved changes' : 'Saved'); gutter(); renderTabs(); renderFiles(); });
|
|
187
|
+
$('edcode').addEventListener('scroll', () => { $('edgutter').scrollTop = $('edcode').scrollTop; });
|
|
188
|
+
['keyup', 'click'].forEach((ev) => $('edcode').addEventListener(ev, caret));
|
|
189
|
+
$('edcode').addEventListener('keydown', (e) => {
|
|
190
|
+
if (e.key === 'Tab' && !e.shiftKey) { // a code editor keeps the focus and inserts two spaces
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
const c = $('edcode'), a = c.selectionStart;
|
|
193
|
+
c.setRangeText(' ', a, c.selectionEnd, 'end');
|
|
194
|
+
c.dispatchEvent(new Event('input'));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
$('editor').addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault(); save(); } });
|
|
198
|
+
$('edsave').addEventListener('click', () => save());
|
|
199
|
+
$('eddiff').addEventListener('click', (e) => { if (e.target.closest('#edaccept')) accept(); else if (e.target.closest('#edreject')) reject(); });
|
|
200
|
+
$('edform').addEventListener('submit', (e) => { e.preventDefault(); const t = $('edask').value.trim(); $('edask').value = ''; ask(t); });
|
|
201
|
+
$('edask').addEventListener('input', () => { $('edsend').disabled = E.busy || !$('edask').value.trim(); });
|
|
202
|
+
$('edask').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); $('edform').requestSubmit(); } });
|
|
203
|
+
$('edstop').addEventListener('click', () => { if (E.ctl) E.ctl.abort(); });
|
|
204
|
+
$('ednew').addEventListener('submit', async (e) => {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
const name = $('ednewname').value.trim();
|
|
207
|
+
if (!name || !has()) return;
|
|
208
|
+
try {
|
|
209
|
+
await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: name, content: '' }) });
|
|
210
|
+
$('ednewname').value = '';
|
|
211
|
+
await refreshFiles();
|
|
212
|
+
const f = E.files.find((x) => x.path.toLowerCase() === name.toLowerCase().replace(/\\/g, '/'));
|
|
213
|
+
await openFile(f ? f.path : name);
|
|
214
|
+
} catch (err) { status('Could not create the file: ' + err.message, true); }
|
|
215
|
+
});
|
|
216
|
+
window.addEventListener('beforeunload', (e) => { if (Object.keys(E.buf).some(dirty)) { e.preventDefault(); e.returnValue = ''; } });
|
|
217
|
+
studio.editor = E;
|
|
218
|
+
});
|
|
219
|
+
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pal/web/static/inter.css
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* Inter (https://rsms.me/inter), SIL Open Font License 1.1: Latin subset, served from this app so no page loads anything from another site. */
|
|
2
|
+
@font-face{font-family:"Inter";font-style:normal;font-weight:400;font-display:swap;src:url("/static/fonts/inter-400.woff2") format("woff2")}
|
|
3
|
+
@font-face{font-family:"Inter";font-style:normal;font-weight:500;font-display:swap;src:url("/static/fonts/inter-500.woff2") format("woff2")}
|
|
4
|
+
@font-face{font-family:"Inter";font-style:normal;font-weight:600;font-display:swap;src:url("/static/fonts/inter-600.woff2") format("woff2")}
|
|
5
|
+
@font-face{font-family:"Inter";font-style:normal;font-weight:700;font-display:swap;src:url("/static/fonts/inter-700.woff2") format("woff2")}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|