@galaxyproject/olit 0.0.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/package.json +61 -0
  4. package/static/InterVariable-Italic.woff2 +0 -0
  5. package/static/InterVariable.woff2 +0 -0
  6. package/static/JetBrainsMono-Bold.woff2 +0 -0
  7. package/static/JetBrainsMono-BoldItalic.woff2 +0 -0
  8. package/static/JetBrainsMono-Italic.woff2 +0 -0
  9. package/static/JetBrainsMono-Regular.woff2 +0 -0
  10. package/static/index.css +1 -0
  11. package/static/index.html +13 -0
  12. package/static/index.js +4024 -0
  13. package/static/logo.svg +31 -0
  14. package/static/olit.xml +98 -0
  15. package/static/pyodide/README.md +62 -0
  16. package/static/pyodide/annotated_types-0.7.0-py3-none-any.whl +0 -0
  17. package/static/pyodide/attrs-25.2.0-py3-none-any.whl +0 -0
  18. package/static/pyodide/console-v2.html +393 -0
  19. package/static/pyodide/console.html +305 -0
  20. package/static/pyodide/ffi.d.ts +1130 -0
  21. package/static/pyodide/galaxy-charts-e7348f1e5617.js +38078 -0
  22. package/static/pyodide/galaxy-ops-6b6069501426.js +9476 -0
  23. package/static/pyodide/jsonschema-4.23.0-py3-none-any.whl +0 -0
  24. package/static/pyodide/jsonschema_specifications-2024.10.1-py3-none-any.whl +0 -0
  25. package/static/pyodide/llm-fetch.js +16 -0
  26. package/static/pyodide/micropip-0.11.1-py3-none-any.whl +0 -0
  27. package/static/pyodide/numpy-2.2.5-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  28. package/static/pyodide/olit-0.0.0-07b000ed515e9-py3-none-any.whl +0 -0
  29. package/static/pyodide/package.json +126 -0
  30. package/static/pyodide/pandas-2.3.3-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  31. package/static/pyodide/pydantic-2.12.5-py3-none-any.whl +0 -0
  32. package/static/pyodide/pydantic_core-2.41.5-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  33. package/static/pyodide/pyodide-lock.json +1 -0
  34. package/static/pyodide/pyodide-worker.js +120 -0
  35. package/static/pyodide/pyodide.asm.js +15 -0
  36. package/static/pyodide/pyodide.asm.wasm +0 -0
  37. package/static/pyodide/pyodide.d.ts +1699 -0
  38. package/static/pyodide/pyodide.js +6 -0
  39. package/static/pyodide/pyodide.js.map +7 -0
  40. package/static/pyodide/pyodide.mjs +4 -0
  41. package/static/pyodide/pyodide.mjs.map +7 -0
  42. package/static/pyodide/pyrsistent-0.20.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  43. package/static/pyodide/python_dateutil-2.9.0.post0-py2.py3-none-any.whl +0 -0
  44. package/static/pyodide/python_stdlib.zip +0 -0
  45. package/static/pyodide/pytz-2025.2-py2.py3-none-any.whl +0 -0
  46. package/static/pyodide/pyyaml-6.0.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  47. package/static/pyodide/referencing-0.36.2-py3-none-any.whl +0 -0
  48. package/static/pyodide/rpds_py-0.30.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl +0 -0
  49. package/static/pyodide/six-1.17.0-py2.py3-none-any.whl +0 -0
  50. package/static/pyodide/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  51. package/static/pyodide/typing_inspection-0.4.2-py3-none-any.whl +0 -0
@@ -0,0 +1,31 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 465 340"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ xmlns:svg="http://www.w3.org/2000/svg">
6
+ <path
7
+ d="m 1.5575189,268.57426 c 3.738,-17.481 15.9770001,-31.997 29.6140001,-43.046 2.778,-2.251 5.633,-4.453 8.347,-6.778 1.218,-1.044 2.43,-2.431 4.103,-2.61 0.159,0.737 -0.006,1.01 -0.395,1.652 -0.962,1.386 -2.104,2.44 -3.216,3.694 -7.785,8.055 -16.384,18.015 -19.467,29.011 -2.632,9.391 0.234,19.1 9.17,23.834 3.159,1.673 6.709,2.908 10.19,3.702 15.889,3.624 32.13,3.459 48.272,2.071 16.599991,-1.426 33.102991,-3.98 49.420991,-7.342 72.135,-14.864 141.91,-42.161 205.915,-78.44 21.984,-12.46 43.219,-26.581 61.819,-43.763 9.267,-8.56 18.611,-18.353 22.876,-30.523 4.065,-11.597 -1.295,-22.218 -11.925,-27.643001 -3.845,-1.962 -8.033,-3.39 -12.161,-4.621 -1.187,-0.355 -3.606,-0.595 -3.306,-2.09 1.263,-0.563 3.126,-0.146 4.481,-0.059 3.699,0.237 7.372,0.703 11.041,1.208 9.817,1.351 19.657,3.862 27.555,10.142001 2.861,2.275 5.464,4.789 6.963,8.204 0.77,1.755 0.955,3.671 1.237,5.543 0.309,2.051 0.063,4.376 -0.345,6.41 -1.731,8.643 -6.461,16.055 -11.713,22.995 -10.277,13.577 -23.557,24.969 -37.249,34.977 -13.504,9.87 -27.738,18.56 -42.136,27.051 -28.085,16.565 -57.429,31.013 -87.527,43.534 -34.289,14.265 -69.126,26.495 -105.042,35.989 -20.327,5.373 -40.931,10.306 -61.728,13.531 -13.713991,2.126 -27.559991,3.218 -41.409991,3.643 -13.051,0.4 -25.998,0.092 -38.82,-2.576 -9.188,-1.911 -20.6920001,-6.719 -23.9020001,-16.399 -1.168,-3.522 -1.207,-7.656 -0.662,-11.301 z"
8
+ opacity="1"
9
+ fill="#e30a17"
10
+ fill-rule="evenodd" />
11
+ <path
12
+ d="m 46.019519,184.09026 c 0.897,-5.833 1.019,-11.713 2.287,-17.52 3.4,-15.581 9.513,-30.15 17.832,-43.73 3.601,-5.88 8.125,-11.318 12.665,-16.482 33.762991,-38.400001 88.721991,-53.111001 138.000991,-41.656001 32.036,7.447 61.237,26.543 79.801,53.935001 6.609,9.75 11.607,20.177 15.441,31.292 3.204,9.29 5.192,19.251 5.98,29.047 0.258,3.211 1.222,7.622 0.207,10.734 -1.558,1.431 -3.681,2.135 -5.514,3.147 -4.029,2.225 -8.128,4.37 -12.25,6.415 -13.896,6.891 -28.037,13.21 -42.29,19.311 -4.662,1.995 -9.362,3.947 -14.075,5.804 -1.063,0.419 -3.896,2.308 -4.918,1.786 6.828,-26.041 7.23,-58.105 -10.584,-80.097 -9.817,-12.121 -24.951,-17.623 -40.016,-19.007 -20.802,-1.912 -41.469,4.72 -54.523,21.598 -11.959,15.462 -13.433,37.786 -12.164,56.517 0.885,13.049 4.377,26.375 12.147,37.089 1.947,2.686 4.111,5.309 6.623,7.481 1.621,1.402 4.176,2.854 5.231,4.758 -0.823,0.89 -3.961,0.932 -5.158,1.165 -4.597,0.896 -9.134,2.1 -13.74,2.936 -14.548,2.639 -29.293,5.174 -44.049991,6.21 -3.73,0.261 -7.445,0.472 -11.182,0.662 -1.984,0.1 -4.478,0.371 -6.416,-0.056 -3.952,-5.263 -6.647,-11.373 -9.245,-17.388 -8.586,-19.877 -11.591,-42.407 -10.09,-63.951 z"
13
+ opacity="1"
14
+ fill="#e30a17"
15
+ fill-rule="evenodd" />
16
+ <path
17
+ d="m 100.74151,311.30126 c 6.182,-1.155 12.488,-1.83 18.705,-2.836 10.061,-1.626 20.096,-3.851 30.02,-6.159 56.495,-13.139 111.887,-32.716 164.046,-58.067 -0.496,3.912 -2.489,8.157 -3.978,11.798 -4.389,10.731 -9.822,20.65 -16.848,29.906 -7.873,10.371 -16.705,19.478 -27.151,27.29 -30.13,22.531 -70.484,30.682 -107.254,23.556 -15.748,-3.052 -30.885,-8.252 -44.746,-16.429 -3.198,-1.886 -6.196,-4.027 -9.266,-6.104 -1.201,-0.812 -2.868,-1.624 -3.528,-2.955 z"
18
+ opacity="1"
19
+ fill="#e30a17"
20
+ fill-rule="evenodd" />
21
+ <path
22
+ d="m 327.97651,6.03826 c 2.41,-5.5070001 9.182,-4.0470001 14.097,-4.0390001 12.769,0.022 25.541,-0.044 38.31,-0.084 4.384,-0.013 11.34,-1.22 13.99,3.233 1.408,2.3670001 0.507,6.6720001 0.529,9.3380001 0.061,7.501 0.029,15.004 0.027,22.506 -0.005,23.666999 -0.008,47.333999 0.057,71.002 0.019,6.998 0.057,14 -0.029,20.998 -0.031,2.533 0.63,6.358 -0.401,8.688 -3.22,3.572 -7.726,6.375 -11.451,9.421 -11.509,9.412 -23.923,17.799 -36.51,25.686 -4.129,2.586 -8.377,4.948 -12.586,7.402 -1.751,1.022 -3.791,2.887 -5.814,3.179 -0.537,-2.01 -0.213,-4.321 -0.209,-6.389 0.008,-3.997 -0.036,-7.996 -0.073,-11.993 -0.123,-13.665 -0.087,-27.333 -0.092,-40.998 -0.01,-26.667 -0.009,-53.334001 -0.022,-80.001001 -0.004,-8.333999 0.005,-16.666999 -0.014,-24.999999 -0.009,-4.122 -0.663,-8.911 0.191,-12.949 z"
23
+ opacity="1"
24
+ fill="#e30a17"
25
+ fill-rule="evenodd" />
26
+ <path
27
+ d="m 327.88051,237.38326 c 8.248,-4.362 16.717,-8.307 24.9,-12.812 14.585,-8.028 28.67,-16.761 42.469,-26.065 0.326,4.787 0.083,9.675 0.091,14.477 0.016,9.333 -0.009,18.666 0.013,27.999 0.02,8.043 -0.387,16.245 5.553,22.539 6.659,7.057 15.927,7.047 24.883,6.986 6.594,-0.044 13.19,-0.078 19.784,-0.11 2.819,-0.013 5.637,0.008 8.456,0.017 1.328,0.004 2.912,-0.233 4.161,0.296 5.278,2.236 3.755,9.178 3.758,13.776 0.005,10.334 0.012,20.668 0.023,31.002 0.003,3.165 0.006,6.331 -0.025,9.497 -0.013,1.375 0.29,3.034 -0.119,4.36 -0.254,0.823 -1.047,2.569 -1.914,2.894 -3.067,1.152 -7.616,0.409 -10.847,0.419 -13.394,0.043 -26.804,0.146 -40.198,0.022 -7.462,-0.068 -14.89,-0.789 -22.067,-2.968 -27.434,-8.327 -48.058,-29.985 -56.063,-57.319 -3.343,-11.415 -2.556,-22.869 -3.085,-34.568 0.076,-0.147 0.152,-0.295 0.227,-0.442 z"
28
+ opacity="1"
29
+ fill="#e30a17"
30
+ fill-rule="evenodd" />
31
+ </svg>
@@ -0,0 +1,98 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE visualization SYSTEM "../../visualization.dtd">
3
+ <visualization name="AI Research Assistant" embeddable="true">
4
+ <description>powered by Olit</description>
5
+ <data_sources>
6
+ <data_source>
7
+ <model_class>HistoryDatasetAssociation</model_class>
8
+ <test test_attr="ext">csv</test>
9
+ <test test_attr="ext">tabular</test>
10
+ <test test_attr="ext">txt</test>
11
+ </data_source>
12
+ </data_sources>
13
+ <params>
14
+ <param required="false">dataset_id</param>
15
+ </params>
16
+ <entry_point entry_point_type="script" src="index.js" css="index.css" />
17
+ <specs>
18
+ <ai_prompt><![CDATA[
19
+ You are Olit, a co-scientist that orchestrates the Galaxy bioinformatics platform.
20
+ You run entirely in the user's browser; you already have their authenticated Galaxy
21
+ session, so there is no connect step. Everything that touches Galaxy or runs code
22
+ happens through a tool call.
23
+
24
+ Most tasks follow the same shape: pick or create a history, get data into it, run a
25
+ Galaxy tool on the data, then read results back from the history.
26
+
27
+ Three kinds of "tool" exist here and are easy to confuse:
28
+ - Your tools (the operations listed to you) are things like get_histories, run_tool,
29
+ invoke_workflow, upload_file_from_url.
30
+ - Galaxy tools (FastQC, Trimmomatic, Bowtie2, ...) are bioinformatics programs inside
31
+ Galaxy. They are NOT in your tool list. Find them with search_tools_by_name or
32
+ search_tools_by_keywords, then execute via run_tool(history_id, tool_id, inputs).
33
+ Use get_tool_input_template(tool_id) to see a tool's expected inputs.
34
+ - Galaxy visualizations (IGV, Plotly, tabulator, ...) are display plugins with their own
35
+ operations: list_visualizations, show_visualization and save_visualization. Showing,
36
+ opening or adding a track to one goes there, never to a tool search.
37
+
38
+ For curated multi-step analyses, prefer search_iwc_workflows to find a vetted
39
+ Intergalactic Workflow Commission (IWC) workflow, then import_workflow_from_iwc and
40
+ invoke_workflow. User-defined tools (created via create_user_tool) are run with
41
+ run_user_tool, not run_tool.
42
+
43
+ Pages are Galaxy-flavored markdown documents: a history-attached page is a "Notebook",
44
+ a standalone page a "Report". Manage them with list_pages / get_page / create_page /
45
+ update_page. Page content embeds datasets through directives that use ENCODED ids
46
+ (e.g. history_dataset_display(history_dataset_id=<encoded-dataset-id>)), which you get
47
+ from get_history_contents / get_dataset_details. Never write a literal id you have not
48
+ read from a tool result in this session.
49
+
50
+ Beyond Galaxy you also have:
51
+ - skills_fetch(repo, path): load operational know-how on demand. The "Skills
52
+ repositories" section below lists what exists and the exact path for each.
53
+ - run_python(code): run Python locally in the browser (Pyodide; numpy and pandas;
54
+ state persists). Use for quick local computation and inspection. It runs in the
55
+ browser, not on Galaxy.
56
+ - vega_dataset(dataset_id, spec): chart one tabular dataset inline with a Vega-Lite
57
+ spec you write. Leave `data` out of it: the spec is pointed at the dataset here.
58
+ - finish(summary): call when the task is complete.
59
+
60
+ You both talk and act. Chat carries dialogue and status — questions, a plan draft
61
+ awaiting approval, a parameter table, a short end-of-turn summary. Tools carry
62
+ everything that touches Galaxy or runs code. A turn that ends in chat is normal and
63
+ expected: that is how you hand a decision back to the user.
64
+
65
+ When the task is complete, call finish with a one-line summary.
66
+ ]]></ai_prompt>
67
+ </specs>
68
+ <help format="markdown"><![CDATA[
69
+ # What is Olit?
70
+
71
+ Olit is a conversational research assistant that helps you plan and run analyses in Galaxy. Ask questions in plain language, explore your data, or describe an analysis you want to perform. For multi-step analyses, Olit proposes a plan for you to review before running Galaxy tools or workflows.
72
+
73
+ After selecting a dataset, a chat panel opens alongside your history. Olit works with your Galaxy data and tools while keeping you in control of what gets executed.
74
+
75
+ ## Key Features
76
+
77
+ - **Plan First, Run Second**: Multi-step analyses are drafted as a plan you can approve, edit, or reject before execution.
78
+ - **Parameter Review**: Tool parameters are shown before execution and can be changed, so you can see exactly what will be submitted.
79
+ - **Runs Real Galaxy Jobs**: Finds and runs installed Galaxy tools and can import and invoke curated workflows from the Intergalactic Workflow Commission (IWC).
80
+ - **Keeps a Record**: Plans, executed steps, and results are recorded in a Galaxy Page attached to your history, keeping the analysis readable and shareable.
81
+ - **Watches Your Jobs**: Submitted jobs and workflow invocations are followed to completion and reported back in the chat.
82
+ - **Local Computation**: Quick calculations and data inspection can run directly in your browser without creating Galaxy jobs.
83
+
84
+ Olit runs in your browser using your existing Galaxy session. It requires no separate server or per-user container and operates with the same Galaxy access available to you.
85
+
86
+ You can use Olit with an LLM service provided by your Galaxy server or connect directly to a supported provider using your own API key. When you provide your own key, it is kept only in the current browser tab and is never sent to or stored by Galaxy.
87
+
88
+ ## Olit and Orbit
89
+
90
+ Olit is closely aligned with Orbit, the original Galaxy AI co-scientist. Its agent model builds on concepts developed for Orbit, including plan-and-approve, parameter review, analysis records, and skills. Olit adapts these concepts to a lightweight, browser-native runtime integrated directly with Galaxy.
91
+
92
+ Orbit is suited to analyses requiring a shell or container environment, while Olit provides the same broader interaction model within a standard Galaxy session.
93
+
94
+ - Orbit: https://github.com/galaxyproject/loom
95
+ - Shared skills corpus: https://github.com/galaxyproject/galaxy-skills
96
+ - Galaxy visualization framework: https://github.com/galaxyproject/galaxy-charts
97
+ ]]></help>
98
+ </visualization>
@@ -0,0 +1,62 @@
1
+ # Pyodide JavaScript package
2
+
3
+ <a href="https://www.npmjs.com/package/pyodide"><img src="https://img.shields.io/npm/v/pyodide" alt="npm"></a>
4
+
5
+ ## Usage
6
+
7
+ Download and extract Pyodide packages from [Github
8
+ releases](https://github.com/pyodide/pyodide/releases)
9
+ (`pyodide-build-*.tar.bz2`). The version of the release needs to match exactly the version of this package.
10
+
11
+ Then you can load Pyodide in Node.js as follows,
12
+
13
+ ```js
14
+ // hello_python.js
15
+ const { loadPyodide } = require("pyodide");
16
+
17
+ async function hello_python() {
18
+ let pyodide = await loadPyodide({
19
+ indexURL: "<pyodide artifacts folder>",
20
+ });
21
+ return pyodide.runPythonAsync("1+1");
22
+ }
23
+
24
+ hello_python().then((result) => {
25
+ console.log("Python says that 1+1 =", result);
26
+ });
27
+ ```
28
+
29
+ ```
30
+ $ node hello_python.js
31
+ Python says that 1+1= 2
32
+ ```
33
+
34
+ Or you can use the REPL. To start the Node.js REPL with support for top level
35
+ await, use `node --experimental-repl-await`:
36
+
37
+ ```
38
+ $ node --experimental-repl-await
39
+ Welcome to Node.js v18.5.0.
40
+ Type ".help" for more information.
41
+ > const { loadPyodide } = require("pyodide");
42
+ undefined
43
+ > let pyodide = await loadPyodide();
44
+ undefined
45
+ > await pyodide.runPythonAsync("1+1");
46
+ 2
47
+ ```
48
+
49
+ ## Details
50
+
51
+ The JavaScript code in this package is responsible for the following tasks:
52
+
53
+ 1. Defines the public [JavaScript API](https://pyodide.org/en/stable/usage/api/js-api.html)
54
+ - Package loading code to allow loading of other Python packages.
55
+ - Can load
56
+ [micropip](https://pyodide.org/en/stable/usage/api/micropip-api.html) to
57
+ bootstrap loading of pure Python wheels
58
+ 2. Loads the CPython interpreter and the core/pyodide emscripten application
59
+ which embeds the interpreter.
60
+ 3. Injects the `js/pyodide` JavaScript API into `sys.modules`. This is the
61
+ final runtime dependency for `core/pyodide` & `py/pyodide`, so after this step
62
+ the interpreter is fully up and running.
@@ -0,0 +1,393 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Pyodide Console v2 (Experimental)
4
+
5
+ This is an experimental version of the Pyodide console that provides
6
+ an enhanced terminal experience using xterm.js. This implementation replaces
7
+ the jQuery Terminal used in the original console with a more feature-rich
8
+ terminal emulator that offers better performance and modern terminal capabilities.
9
+
10
+ Note: This console is still under development and may not have all the features
11
+ of the stable console.
12
+ -->
13
+ <html>
14
+ <head>
15
+ <title>Pyodide Console</title>
16
+ <meta charset="UTF-8" />
17
+ <meta
18
+ http-equiv="origin-trial"
19
+ content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0="
20
+ />
21
+ <meta
22
+ http-equiv="origin-trial"
23
+ content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0="
24
+ />
25
+ <link
26
+ rel="stylesheet"
27
+ href="https://unpkg.com/@xterm/xterm@5.4.0/css/xterm.css"
28
+ />
29
+ <link
30
+ href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐍</text></svg>"
31
+ rel="icon"
32
+ />
33
+ <script src="https://unpkg.com/@xterm/xterm@5.4.0/lib/xterm.js"></script>
34
+ <script src="https://unpkg.com/@xterm/addon-fit@0.9.0/lib/addon-fit.js"></script>
35
+ <style>
36
+ * {
37
+ margin: 0;
38
+ padding: 0;
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ html,
43
+ body {
44
+ margin: 0;
45
+ background-color: #000000;
46
+ font-family: "Monaco", "Menlo", "Courier New", monospace;
47
+ overflow: hidden;
48
+ }
49
+
50
+ #terminal {
51
+ position: fixed;
52
+ inset: 10px;
53
+ }
54
+
55
+ #loading {
56
+ display: inline-block;
57
+ width: 50px;
58
+ height: 50px;
59
+ position: fixed;
60
+ top: 50%;
61
+ left: 50%;
62
+ border: 3px solid rgba(172, 237, 255, 0.5);
63
+ border-radius: 50%;
64
+ border-top-color: #fff;
65
+ animation: spin 1s ease-in-out infinite;
66
+ -webkit-animation: spin 1s ease-in-out infinite;
67
+ }
68
+
69
+ @keyframes spin {
70
+ to {
71
+ -webkit-transform: rotate(360deg);
72
+ }
73
+ }
74
+ @-webkit-keyframes spin {
75
+ to {
76
+ -webkit-transform: rotate(360deg);
77
+ }
78
+ }
79
+ </style>
80
+ </head>
81
+ <body>
82
+ <div id="loading"></div>
83
+ <div id="terminal"></div>
84
+ <script type="module">
85
+ async function main() {
86
+ const fitAddon = new FitAddon.FitAddon();
87
+ const term = new Terminal({
88
+ cursorBlink: true,
89
+ cursorStyle: "block",
90
+ convertEol: true,
91
+ scrollback: 2_000,
92
+ fontSize: 18,
93
+ lineHeight: 1.4,
94
+ fontFamily: "monospace",
95
+ theme: {
96
+ background: "#000000",
97
+ foreground: "rgba(255, 255, 255, 0.8)",
98
+ cursor: "rgba(255, 255, 255, 0.8)",
99
+ selection: "#404040",
100
+ error: "#ff0000",
101
+ },
102
+ });
103
+ window.term = term;
104
+
105
+ term.open(document.getElementById("terminal"));
106
+ term.loadAddon(fitAddon);
107
+
108
+ fitAddon.fit();
109
+ term.focus();
110
+
111
+ window.addEventListener("resize", () => {
112
+ setTimeout(() => fitAddon.fit(), 50);
113
+ });
114
+
115
+ // Re-fit after the page has fully loaded
116
+ window.addEventListener("load", () => {
117
+ setTimeout(() => fitAddon.fit(), 100);
118
+ });
119
+
120
+ // Initialize Pyodide
121
+ let indexURL = "./";
122
+ const urlParams = new URLSearchParams(window.location.search);
123
+ const buildParam = urlParams.get("build");
124
+ if (buildParam && ["full", "debug", "pyc"].includes(buildParam)) {
125
+ indexURL = indexURL.replace("/full/", "/" + buildParam + "/");
126
+ }
127
+
128
+ const { loadPyodide } = await import(indexURL + "pyodide.mjs");
129
+ const pyodide = await loadPyodide();
130
+ globalThis.pyodide = pyodide;
131
+
132
+ // Hide loading spinner
133
+ document.getElementById("loading").style.display = "none";
134
+
135
+ const { repr_shorten, BANNER, PyodideConsole } =
136
+ pyodide.pyimport("pyodide.console");
137
+
138
+ term.writeln(
139
+ `Welcome to the Pyodide ${pyodide.version} terminal emulator 🐍\n${BANNER}`
140
+ );
141
+
142
+ const pyconsole = PyodideConsole(pyodide.globals);
143
+
144
+ const namespace = pyodide.globals.get("dict")();
145
+ const await_fut = pyodide.runPython(
146
+ `
147
+ import builtins
148
+ from pyodide.ffi import to_js
149
+ async def await_fut(fut):
150
+ res = await fut
151
+ if res is not None:
152
+ builtins._ = res
153
+ return to_js([res], depth=1)
154
+ await_fut
155
+ `,
156
+ { globals: namespace }
157
+ );
158
+ namespace.destroy();
159
+
160
+ pyconsole.stdout_callback = (s) => term.write(s);
161
+ pyconsole.stderr_callback = (s) => term.write(`\x1b[31m${s}\x1b[0m`);
162
+
163
+ // Handle fatal errors
164
+ pyodide._api.on_fatal = async (e) => {
165
+ if (e.name === "Exit") {
166
+ term.write(`\x1b[31m${e}\x1b[0m\r\n`);
167
+ term.write(
168
+ "\x1b[31mPyodide exited and can no longer be used.\x1b[0m\r\n"
169
+ );
170
+ } else {
171
+ term.write(
172
+ "\x1b[31mPyodide has suffered a fatal error. Please report this to the Pyodide maintainers.\x1b[0m\r\n"
173
+ );
174
+ term.write("\x1b[31mThe cause of the fatal error was:\x1b[0m\r\n");
175
+ term.write(`\x1b[31m${e.message || e}\x1b[0m\r\n`);
176
+ term.write(
177
+ "\x1b[31mLook in the browser console for more details.\x1b[0m\r\n"
178
+ );
179
+ }
180
+ };
181
+
182
+ // REPL implementation
183
+ const ps1 = ">>> ";
184
+ const ps2 = "... ";
185
+ let buffer = "";
186
+ let cursorIndex = 0; // index within buffer for in-line editing
187
+ let prompt = ps1;
188
+ const history = [];
189
+ let historyIndex = null; // null means not navigating history
190
+
191
+ term.write(prompt);
192
+
193
+ function addToHistory(command) {
194
+ const trimmed = command.trimEnd();
195
+ if (!trimmed) return;
196
+ const last = history[history.length - 1];
197
+ if (last !== trimmed) history.push(trimmed);
198
+ }
199
+
200
+ function refreshLine() {
201
+ // Write left part, save cursor, write right part, clear, restore cursor.
202
+ const clearCommand = "\x1b[0K";
203
+ const leftPart = prompt + buffer.slice(0, cursorIndex);
204
+ const rightPart = buffer.slice(cursorIndex);
205
+ term.write(
206
+ `\x1b[0G${leftPart}\x1b[s${rightPart}${clearCommand}\x1b[u`
207
+ );
208
+ }
209
+
210
+ function setBuffer(newBuffer, newCursorIndex = null) {
211
+ buffer = newBuffer;
212
+ if (newCursorIndex === null) {
213
+ cursorIndex = buffer.length;
214
+ } else {
215
+ cursorIndex = Math.max(0, Math.min(newCursorIndex, buffer.length));
216
+ }
217
+ refreshLine();
218
+ }
219
+
220
+ async function execLine(line) {
221
+ // Normalize non-breaking spaces to regular spaces
222
+ line = line.replace(/\u00a0/g, " ");
223
+ // clear the terminal
224
+ if (line === "clear") {
225
+ term.clear();
226
+ return;
227
+ }
228
+
229
+ const fut = pyconsole.push(line);
230
+
231
+ switch (fut.syntax_check) {
232
+ case "syntax-error":
233
+ term.write(`\x1b[31m${fut.formatted_error.trimEnd()}\x1b[0m`);
234
+ term.write("\r\n");
235
+ prompt = ps1;
236
+ addToHistory(line);
237
+ historyIndex = null;
238
+ fut.destroy();
239
+ break;
240
+ case "incomplete":
241
+ prompt = ps2;
242
+ addToHistory(line);
243
+ historyIndex = null;
244
+ return;
245
+ case "complete":
246
+ prompt = ps1;
247
+ try {
248
+ const wrapped = await_fut(fut);
249
+ const [value] = await wrapped;
250
+ if (value !== undefined) {
251
+ const output = repr_shorten.callKwargs(value, {
252
+ separator: "\n<long output truncated>\n",
253
+ });
254
+ term.write(output);
255
+ term.write("\r\n");
256
+ }
257
+ if (value instanceof pyodide.ffi.PyProxy) value.destroy();
258
+ wrapped.destroy();
259
+ } catch (e) {
260
+ const msg = fut.formatted_error || e.message;
261
+ term.write(`\x1b[31m${String(msg).trimEnd()}\x1b[0m`);
262
+ term.write("\r\n");
263
+ } finally {
264
+ fut.destroy();
265
+ }
266
+ addToHistory(line);
267
+ historyIndex = null;
268
+ break;
269
+ default:
270
+ term.write(
271
+ `\r\nUnexpected syntax_check value: ${fut.syntax_check}`
272
+ );
273
+ }
274
+ }
275
+
276
+ term.onData(async (data) => {
277
+ switch (data) {
278
+ case "\r": // Enter
279
+ term.write("\r\n");
280
+ await execLine(buffer);
281
+ buffer = "";
282
+ cursorIndex = 0;
283
+ term.write(prompt);
284
+ break;
285
+ case "\u0003": // Ctrl-C
286
+ pyconsole.buffer.clear();
287
+ buffer = "";
288
+ cursorIndex = 0;
289
+ term.write("^C\r\nKeyboardInterrupt\r\n" + ps1);
290
+ prompt = ps1;
291
+ historyIndex = null;
292
+ break;
293
+ case "\u0016": // Ctrl-V
294
+ // paste from clipboard
295
+ const clipboard = await navigator.clipboard.readText();
296
+ const newBuf =
297
+ buffer.slice(0, cursorIndex) +
298
+ clipboard +
299
+ buffer.slice(cursorIndex);
300
+ setBuffer(newBuf, newBuf.length);
301
+ break;
302
+ case "\u007F": // Backspace
303
+ if (cursorIndex > 0) {
304
+ const before = buffer.slice(0, cursorIndex - 1);
305
+ const after = buffer.slice(cursorIndex);
306
+ cursorIndex -= 1;
307
+ setBuffer(before + after, cursorIndex);
308
+ }
309
+ break;
310
+ case "\x1B[A": // Up arrow
311
+ if (prompt === ps1) {
312
+ if (historyIndex === null) historyIndex = history.length;
313
+ if (historyIndex > 0) {
314
+ historyIndex -= 1;
315
+ const newBuf = history[historyIndex] || "";
316
+ setBuffer(newBuf, newBuf.length);
317
+ }
318
+ }
319
+ break;
320
+ case "\x1B[B": // Down arrow
321
+ if (prompt === ps1 && historyIndex !== null) {
322
+ if (historyIndex < history.length - 1) {
323
+ historyIndex += 1;
324
+ const newBuf = history[historyIndex] || "";
325
+ setBuffer(newBuf, newBuf.length);
326
+ } else {
327
+ historyIndex = null;
328
+ setBuffer("", 0);
329
+ }
330
+ }
331
+ break;
332
+ case "\x1B[C": // Right arrow
333
+ if (cursorIndex < buffer.length) {
334
+ cursorIndex += 1;
335
+ refreshLine();
336
+ }
337
+ break;
338
+ case "\x1B[D": // Left arrow
339
+ if (cursorIndex > 0) {
340
+ cursorIndex -= 1;
341
+ refreshLine();
342
+ }
343
+ break;
344
+ default:
345
+ if (data) {
346
+ // Normalize non-breaking spaces to regular spaces
347
+ data = data.replace(/\u00a0/g, " ");
348
+ // Insert arbitrary string at cursor position
349
+ const before = buffer.slice(0, cursorIndex);
350
+ const after = buffer.slice(cursorIndex);
351
+ const newBuf = before + data + after;
352
+ const newCursor = cursorIndex + data.length;
353
+ setBuffer(newBuf, newCursor);
354
+ }
355
+ }
356
+ });
357
+
358
+ // 4. Extra features
359
+ let idbkvPromise;
360
+ async function getIDBKV() {
361
+ if (!idbkvPromise) {
362
+ idbkvPromise = await import(
363
+ "https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js"
364
+ );
365
+ }
366
+ return idbkvPromise;
367
+ }
368
+
369
+ async function mountDirectory(pyodideDirectory, directoryKey) {
370
+ if (pyodide.FS.analyzePath(pyodideDirectory).exists) {
371
+ return;
372
+ }
373
+ const { get, set } = await getIDBKV();
374
+ const opts = { id: "mountdirid", mode: "readwrite" };
375
+ let directoryHandle = await get(directoryKey);
376
+ if (!directoryHandle) {
377
+ directoryHandle = await showDirectoryPicker(opts);
378
+ await set(directoryKey, directoryHandle);
379
+ }
380
+ const permissionStatus = await directoryHandle.requestPermission(
381
+ opts
382
+ );
383
+ if (permissionStatus !== "granted") {
384
+ throw new Error("readwrite access to directory not granted");
385
+ }
386
+ await pyodide.mountNativeFS(pyodideDirectory, directoryHandle);
387
+ }
388
+ globalThis.mountDirectory = mountDirectory;
389
+ }
390
+ window.console_ready = main();
391
+ </script>
392
+ </body>
393
+ </html>