pi-studio 0.5.38 → 0.5.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.40] — 2026-03-30
8
+
9
+ ### Changed
10
+ - Studio editor highlighting now keeps plain text in code files closer to the normal editor foreground while preserving a distinct markdown-code tint for inline backticks and unlabeled fenced blocks.
11
+ - Language-aware Studio syntax highlighting for code files and labeled fenced code blocks is now a bit richer, with additional highlighting for function-like identifiers, type/class-like names, and a few language-specific constructs such as decorators or macros.
12
+
13
+ ## [0.5.39] — 2026-03-30
14
+
15
+ ### Added
16
+ - Studio now supports the familiar `Cmd/Ctrl+S` shortcut for saving editor content.
17
+
18
+ ### Changed
19
+ - `Cmd/Ctrl+S` now triggers **Save editor** when a direct save path is available, and falls back to **Save editor as…** otherwise.
20
+ - Save button tooltips and the footer shortcut hint now advertise the save shortcut explicitly.
21
+
7
22
  ## [0.5.38] — 2026-03-29
8
23
 
9
24
  ### Added
@@ -878,6 +878,19 @@
878
878
  return true;
879
879
  }
880
880
 
881
+ function triggerEditorSaveShortcut() {
882
+ if (saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
883
+ saveOverBtn.click();
884
+ return true;
885
+ }
886
+ if (saveAsBtn && !saveAsBtn.disabled && !saveAsBtn.hidden) {
887
+ saveAsBtn.click();
888
+ return true;
889
+ }
890
+ setStatus("Save is unavailable right now.", "warning");
891
+ return false;
892
+ }
893
+
881
894
  function handlePaneShortcut(event) {
882
895
  if (!event || event.defaultPrevented) return;
883
896
 
@@ -914,6 +927,18 @@
914
927
  return;
915
928
  }
916
929
 
930
+ const isSaveShortcut =
931
+ key.toLowerCase() === "s"
932
+ && (event.metaKey || event.ctrlKey)
933
+ && !event.altKey
934
+ && !event.shiftKey;
935
+
936
+ if (isSaveShortcut) {
937
+ event.preventDefault();
938
+ triggerEditorSaveShortcut();
939
+ return;
940
+ }
941
+
917
942
  if (plainEscape) {
918
943
  const activeKind = getAbortablePendingKind();
919
944
  if (activeKind === "direct" || activeKind === "critique") {
@@ -2525,11 +2550,11 @@
2525
2550
 
2526
2551
  var effectivePath = getEffectiveSavePath();
2527
2552
  if (effectivePath) {
2528
- saveOverBtn.title = "Overwrite file: " + effectivePath;
2553
+ saveOverBtn.title = "Overwrite file: " + effectivePath + " · Shortcut: Cmd/Ctrl+S.";
2529
2554
  return;
2530
2555
  }
2531
2556
 
2532
- saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…";
2557
+ saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…. Shortcut: Cmd/Ctrl+S falls back to Save editor as when needed.";
2533
2558
  }
2534
2559
 
2535
2560
  function syncActionButtons() {
@@ -2743,7 +2768,7 @@
2743
2768
  }
2744
2769
 
2745
2770
  if (match[1]) {
2746
- out += wrapHighlight("hl-code", token);
2771
+ out += wrapHighlight("hl-md-code", token);
2747
2772
  } else if (match[2]) {
2748
2773
  const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
2749
2774
  if (linkMatch) {
@@ -2845,37 +2870,43 @@
2845
2870
  }
2846
2871
 
2847
2872
  if (lang === "javascript" || lang === "typescript") {
2848
- const jsPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|from|export|default|async|await|true|false|null|undefined|typeof|instanceof)\b)|(\b\d+(?:\.\d+)?\b)/g;
2873
+ const jsPattern = /(\/\/.*$|\/\*.*?\*\/)|(`(?:[^`\\]|\\.)*`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|from|export|default|async|await|true|false|null|undefined|typeof|instanceof|interface|implements|enum|type|public|private|protected|readonly|abstract|declare|this|super)\b)|(\b[A-Za-z_$][A-Za-z0-9_$]*(?=\s*\())|(\b[A-Z][A-Za-z0-9_$]*\b)|(\b\d+(?:\.\d+)?\b)/g;
2849
2874
  const highlighted = highlightCodeTokens(source, jsPattern, (match) => {
2850
2875
  if (match[1]) return "hl-code-com";
2851
2876
  if (match[2]) return "hl-code-str";
2852
2877
  if (match[3]) return "hl-code-kw";
2853
- if (match[4]) return "hl-code-num";
2878
+ if (match[4]) return "hl-code-fn";
2879
+ if (match[5]) return "hl-code-type";
2880
+ if (match[6]) return "hl-code-num";
2854
2881
  return "hl-code";
2855
2882
  });
2856
2883
  return "<span class='hl-code'>" + highlighted + "</span>";
2857
2884
  }
2858
2885
 
2859
2886
  if (lang === "python") {
2860
- const pyPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:def|class|return|if|elif|else|for|while|try|except|finally|import|from|as|with|lambda|yield|True|False|None|and|or|not|in|is|pass|break|continue|raise|global|nonlocal|assert)\b)|(\b\d+(?:\.\d+)?\b)/g;
2887
+ const pyPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(@[A-Za-z_][A-Za-z0-9_]*)|(\b(?:def|class|return|if|elif|else|for|while|try|except|finally|import|from|as|with|lambda|yield|True|False|None|and|or|not|in|is|pass|break|continue|raise|global|nonlocal|assert)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\())|(\b[A-Z][A-Za-z0-9_]*\b)|(\b\d+(?:\.\d+)?\b)/g;
2861
2888
  const highlighted = highlightCodeTokens(source, pyPattern, (match) => {
2862
2889
  if (match[1]) return "hl-code-com";
2863
2890
  if (match[2]) return "hl-code-str";
2864
- if (match[3]) return "hl-code-kw";
2865
- if (match[4]) return "hl-code-num";
2891
+ if (match[3]) return "hl-code-fn";
2892
+ if (match[4]) return "hl-code-kw";
2893
+ if (match[5]) return "hl-code-fn";
2894
+ if (match[6]) return "hl-code-type";
2895
+ if (match[7]) return "hl-code-num";
2866
2896
  return "hl-code";
2867
2897
  });
2868
2898
  return "<span class='hl-code'>" + highlighted + "</span>";
2869
2899
  }
2870
2900
 
2871
2901
  if (lang === "bash") {
2872
- const shPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'[^']*')|(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*)|(\b(?:if|then|else|fi|for|in|do|done|case|esac|function|local|export|readonly|return|break|continue|while|until)\b)|(\b\d+\b)/g;
2902
+ const shPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'[^']*')|(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*)|(\b(?:if|then|else|fi|for|in|do|done|case|esac|function|local|export|readonly|return|break|continue|while|until)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\(\s*\)))|(\b\d+\b)/g;
2873
2903
  const highlighted = highlightCodeTokens(source, shPattern, (match) => {
2874
2904
  if (match[1]) return "hl-code-com";
2875
2905
  if (match[2]) return "hl-code-str";
2876
2906
  if (match[3]) return "hl-code-var";
2877
2907
  if (match[4]) return "hl-code-kw";
2878
- if (match[5]) return "hl-code-num";
2908
+ if (match[5]) return "hl-code-fn";
2909
+ if (match[6]) return "hl-code-num";
2879
2910
  return "hl-code";
2880
2911
  });
2881
2912
  return "<span class='hl-code'>" + highlighted + "</span>";
@@ -2894,74 +2925,85 @@
2894
2925
  }
2895
2926
 
2896
2927
  if (lang === "rust") {
2897
- const rustPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*")|(\b(?:fn|let|mut|const|struct|enum|impl|trait|pub|mod|use|crate|self|super|match|if|else|for|while|loop|return|break|continue|where|as|in|ref|move|async|await|unsafe|extern|type|static|true|false|Some|None|Ok|Err|Self)\b)|(\b\d[\d_]*(?:\.\d[\d_]*)?(?:f32|f64|u8|u16|u32|u64|u128|usize|i8|i16|i32|i64|i128|isize)?\b)/g;
2928
+ const rustPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*")|(\b[A-Za-z_][A-Za-z0-9_]*!(?=\s*(?:\(|\{|\[)))|(\b(?:fn|let|mut|const|struct|enum|impl|trait|pub|mod|use|crate|self|super|match|if|else|for|while|loop|return|break|continue|where|as|in|ref|move|async|await|unsafe|extern|type|static|true|false|Some|None|Ok|Err|Self)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\())|(\b[A-Z][A-Za-z0-9_]*\b)|(\b\d[\d_]*(?:\.\d[\d_]*)?(?:f32|f64|u8|u16|u32|u64|u128|usize|i8|i16|i32|i64|i128|isize)?\b)/g;
2898
2929
  const highlighted = highlightCodeTokens(source, rustPattern, (match) => {
2899
2930
  if (match[1]) return "hl-code-com";
2900
2931
  if (match[2]) return "hl-code-str";
2901
- if (match[3]) return "hl-code-kw";
2902
- if (match[4]) return "hl-code-num";
2932
+ if (match[3]) return "hl-code-fn";
2933
+ if (match[4]) return "hl-code-kw";
2934
+ if (match[5]) return "hl-code-fn";
2935
+ if (match[6]) return "hl-code-type";
2936
+ if (match[7]) return "hl-code-num";
2903
2937
  return "hl-code";
2904
2938
  });
2905
2939
  return "<span class='hl-code'>" + highlighted + "</span>";
2906
2940
  }
2907
2941
 
2908
2942
  if (lang === "c" || lang === "cpp") {
2909
- const cPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)')|(#\s*\w+)|(\b(?:if|else|for|while|do|switch|case|break|continue|return|goto|struct|union|enum|typedef|sizeof|void|int|char|short|long|float|double|unsigned|signed|const|static|extern|volatile|register|inline|auto|restrict|true|false|NULL|nullptr|class|public|private|protected|virtual|override|template|typename|namespace|using|new|delete|try|catch|throw|noexcept|constexpr|auto|decltype|static_cast|dynamic_cast|reinterpret_cast|const_cast|std|include|define|ifdef|ifndef|endif|pragma)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFlLuU]*\b)/g;
2943
+ const cPattern = /(\/\/.*$|\/\*.*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)')|(#\s*\w+)|(\b(?:if|else|for|while|do|switch|case|break|continue|return|goto|struct|union|enum|typedef|sizeof|void|int|char|short|long|float|double|unsigned|signed|const|static|extern|volatile|register|inline|auto|restrict|true|false|NULL|nullptr|class|public|private|protected|virtual|override|template|typename|namespace|using|new|delete|try|catch|throw|noexcept|constexpr|auto|decltype|static_cast|dynamic_cast|reinterpret_cast|const_cast|std|include|define|ifdef|ifndef|endif|pragma)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\())|(\b[A-Z][A-Za-z0-9_]*\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFlLuU]*\b)/g;
2910
2944
  const highlighted = highlightCodeTokens(source, cPattern, (match) => {
2911
2945
  if (match[1]) return "hl-code-com";
2912
2946
  if (match[2]) return "hl-code-str";
2913
2947
  if (match[3]) return "hl-code-kw";
2914
2948
  if (match[4]) return "hl-code-kw";
2915
- if (match[5]) return "hl-code-num";
2949
+ if (match[5]) return "hl-code-fn";
2950
+ if (match[6]) return "hl-code-type";
2951
+ if (match[7]) return "hl-code-num";
2916
2952
  return "hl-code";
2917
2953
  });
2918
2954
  return "<span class='hl-code'>" + highlighted + "</span>";
2919
2955
  }
2920
2956
 
2921
2957
  if (lang === "julia") {
2922
- const jlPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:function|end|if|elseif|else|for|while|begin|let|local|global|const|return|break|continue|do|try|catch|finally|throw|module|import|using|export|struct|mutable|abstract|primitive|where|macro|quote|true|false|nothing|missing|in|isa|typeof)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
2958
+ const jlPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(@[A-Za-z_][A-Za-z0-9_]*)|(\b(?:function|end|if|elseif|else|for|while|begin|let|local|global|const|return|break|continue|do|try|catch|finally|throw|module|import|using|export|struct|mutable|abstract|primitive|where|macro|quote|true|false|nothing|missing|in|isa|typeof)\b)|(\b[A-Za-z_][A-Za-z0-9_]*!?(?=\s*\())|(\b[A-Z][A-Za-z0-9_]*\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
2923
2959
  const highlighted = highlightCodeTokens(source, jlPattern, (match) => {
2924
2960
  if (match[1]) return "hl-code-com";
2925
2961
  if (match[2]) return "hl-code-str";
2926
- if (match[3]) return "hl-code-kw";
2927
- if (match[4]) return "hl-code-num";
2962
+ if (match[3]) return "hl-code-fn";
2963
+ if (match[4]) return "hl-code-kw";
2964
+ if (match[5]) return "hl-code-fn";
2965
+ if (match[6]) return "hl-code-type";
2966
+ if (match[7]) return "hl-code-num";
2928
2967
  return "hl-code";
2929
2968
  });
2930
2969
  return "<span class='hl-code'>" + highlighted + "</span>";
2931
2970
  }
2932
2971
 
2933
2972
  if (lang === "fortran") {
2934
- const fPattern = /(!.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:program|end|subroutine|function|module|use|implicit|none|integer|real|double|precision|complex|character|logical|dimension|allocatable|intent|in|out|inout|parameter|data|do|if|then|else|elseif|endif|enddo|call|return|write|read|print|format|stop|contains|type|class|select|case|where|forall|associate|block|procedure|interface|abstract|extends|allocate|deallocate|cycle|exit|go|to|common|equivalence|save|external|intrinsic)\b)|(\b\d+(?:\.\d+)?(?:[dDeE][+-]?\d+)?\b)/gi;
2973
+ const fPattern = /(!.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:program|end|subroutine|function|module|use|implicit|none|integer|real|double|precision|complex|character|logical|dimension|allocatable|intent|in|out|inout|parameter|data|do|if|then|else|elseif|endif|enddo|call|return|write|read|print|format|stop|contains|type|class|select|case|where|forall|associate|block|procedure|interface|abstract|extends|allocate|deallocate|cycle|exit|go|to|common|equivalence|save|external|intrinsic)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\())|(\b\d+(?:\.\d+)?(?:[dDeE][+-]?\d+)?\b)/gi;
2935
2974
  const highlighted = highlightCodeTokens(source, fPattern, (match) => {
2936
2975
  if (match[1]) return "hl-code-com";
2937
2976
  if (match[2]) return "hl-code-str";
2938
2977
  if (match[3]) return "hl-code-kw";
2939
- if (match[4]) return "hl-code-num";
2978
+ if (match[4]) return "hl-code-fn";
2979
+ if (match[5]) return "hl-code-num";
2940
2980
  return "hl-code";
2941
2981
  });
2942
2982
  return "<span class='hl-code'>" + highlighted + "</span>";
2943
2983
  }
2944
2984
 
2945
2985
  if (lang === "r") {
2946
- const rPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:function|if|else|for|while|repeat|in|next|break|return|TRUE|FALSE|NULL|NA|NA_integer_|NA_real_|NA_complex_|NA_character_|Inf|NaN|library|require|source|local|switch)\b)|(<-|->|<<-|->>)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[Li]?\b)/g;
2986
+ const rPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:function|if|else|for|while|repeat|in|next|break|return|TRUE|FALSE|NULL|NA|NA_integer_|NA_real_|NA_complex_|NA_character_|Inf|NaN|library|require|source|local|switch)\b)|(<-|->|<<-|->>)|(\b[A-Za-z.][A-Za-z0-9._]*(?=\s*\())|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[Li]?\b)/g;
2947
2987
  const highlighted = highlightCodeTokens(source, rPattern, (match) => {
2948
2988
  if (match[1]) return "hl-code-com";
2949
2989
  if (match[2]) return "hl-code-str";
2950
2990
  if (match[3]) return "hl-code-kw";
2951
- if (match[4]) return "hl-code-kw";
2952
- if (match[5]) return "hl-code-num";
2991
+ if (match[4]) return "hl-code-op";
2992
+ if (match[5]) return "hl-code-fn";
2993
+ if (match[6]) return "hl-code-num";
2953
2994
  return "hl-code";
2954
2995
  });
2955
2996
  return "<span class='hl-code'>" + highlighted + "</span>";
2956
2997
  }
2957
2998
 
2958
2999
  if (lang === "matlab") {
2959
- const matPattern = /(%.*$)|('(?:[^']|'')*'|"(?:[^"\\]|\\.)*")|(\b(?:function|end|if|elseif|else|for|while|switch|case|otherwise|try|catch|return|break|continue|global|persistent|classdef|properties|methods|events|enumeration|true|false)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[i]?\b)/g;
3000
+ const matPattern = /(%.*$)|('(?:[^']|'')*'|"(?:[^"\\]|\\.)*")|(\b(?:function|end|if|elseif|else|for|while|switch|case|otherwise|try|catch|return|break|continue|global|persistent|classdef|properties|methods|events|enumeration|true|false)\b)|(\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\())|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[i]?\b)/g;
2960
3001
  const highlighted = highlightCodeTokens(source, matPattern, (match) => {
2961
3002
  if (match[1]) return "hl-code-com";
2962
3003
  if (match[2]) return "hl-code-str";
2963
3004
  if (match[3]) return "hl-code-kw";
2964
- if (match[4]) return "hl-code-num";
3005
+ if (match[4]) return "hl-code-fn";
3006
+ if (match[5]) return "hl-code-num";
2965
3007
  return "hl-code";
2966
3008
  });
2967
3009
  return "<span class='hl-code'>" + highlighted + "</span>";
@@ -3059,7 +3101,13 @@
3059
3101
  }
3060
3102
 
3061
3103
  if (inFence) {
3062
- out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : EMPTY_OVERLAY_LINE);
3104
+ if (line.length === 0) {
3105
+ out.push(EMPTY_OVERLAY_LINE);
3106
+ } else if (fenceLanguage) {
3107
+ out.push(highlightCodeLine(line, fenceLanguage));
3108
+ } else {
3109
+ out.push(wrapHighlight("hl-md-code", line));
3110
+ }
3063
3111
  continue;
3064
3112
  }
3065
3113
 
package/client/studio.css CHANGED
@@ -482,6 +482,10 @@
482
482
  }
483
483
 
484
484
  .hl-code {
485
+ color: var(--text);
486
+ }
487
+
488
+ .hl-md-code {
485
489
  color: var(--md-code);
486
490
  }
487
491
 
@@ -508,6 +512,18 @@
508
512
  color: var(--syntax-variable);
509
513
  }
510
514
 
515
+ .hl-code-fn {
516
+ color: var(--syntax-function);
517
+ }
518
+
519
+ .hl-code-type {
520
+ color: var(--syntax-type);
521
+ }
522
+
523
+ .hl-code-op {
524
+ color: var(--syntax-operator);
525
+ }
526
+
511
527
  .hl-diff-add {
512
528
  color: var(--ok);
513
529
  background: rgba(46, 160, 67, 0.12);
package/index.ts CHANGED
@@ -5732,8 +5732,8 @@ ${cssVarsBlock}
5732
5732
  <header>
5733
5733
  <h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
5734
5734
  <div class="controls">
5735
- <button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
5736
- <button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
5735
+ <button id="saveAsBtn" type="button" title="Save editor content to a new file path. Cmd/Ctrl+S falls back here when no direct save path is available.">Save editor as…</button>
5736
+ <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
5737
5737
  <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
5738
5738
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
5739
5739
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
@@ -5884,7 +5884,7 @@ ${cssVarsBlock}
5884
5884
  <footer>
5885
5885
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
5886
5886
  <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
5887
- <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
5887
+ <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Save editor: Cmd/Ctrl+S · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
5888
5888
  </footer>
5889
5889
 
5890
5890
  <div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.38",
3
+ "version": "0.5.40",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",