tina4ruby 3.13.149 → 3.13.150
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/tina4/dev_admin.rb +14 -0
- data/lib/tina4/metrics.rb +5 -1
- data/lib/tina4/plan.rb +1 -1
- data/lib/tina4/public/bundled-component-provenance.json +2 -2
- data/lib/tina4/public/js/tina4-dev-admin-split.html +202 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +147 -138
- data/lib/tina4/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e63d80b48d2821411c3174e3307176cdf996bfc722c2c228be2b829df8a898c3
|
|
4
|
+
data.tar.gz: 4323b78fdba05283005597a4c8489d29a8b74cb02754bc9ec7b4c88f361d0cbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68483df7f4ae92df44d121678397b0c6a4884277e88c7321fc0ed57ba36e34ae26bb8bca3f148eed1904127907d2a0c20369ca729341131ebea409a3e47b799b
|
|
7
|
+
data.tar.gz: aa95f3c686a89537e480bdc2d49de82a1d8cec58dfe0e1c24398f8f19ea3552ba9ec2eac8a351ef859971400ed0756afe2e8421bcd4461ee4c02ffce21e6f715
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.150 — 2026-10-11
|
|
10
|
+
|
|
11
|
+
### Dev admin
|
|
12
|
+
- The dashboard AI talks to the `tina4 ai serve` gateway on :11435, not a model
|
|
13
|
+
directly; `/__dev/split` serves the shared split-preview asset; the scaffold
|
|
14
|
+
panel is data-driven from the CLI generators; and the Code Landscape metrics
|
|
15
|
+
bubbles are free to spread (zoom-out + Fit frame the layout, was crammed into
|
|
16
|
+
the viewport). The dashboard metrics scan passes `--no-history` so it stops
|
|
17
|
+
self-reloading. Shipped in the shared SPA bundle.
|
|
18
|
+
|
|
9
19
|
## 3.13.149 — 2026-10-10
|
|
10
20
|
|
|
11
21
|
### CRUD
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -439,6 +439,8 @@ module Tina4
|
|
|
439
439
|
serve_dashboard
|
|
440
440
|
when ["GET", "/__dev/js/tina4-dev-admin.min.js"]
|
|
441
441
|
serve_dev_js
|
|
442
|
+
when ["GET", "/__dev/split"], ["GET", "/__dev/split/"]
|
|
443
|
+
serve_split
|
|
442
444
|
when ["GET", "/__dev/toolbar.css"]
|
|
443
445
|
# tina4stack #115: the injected toolbar's stylesheet, served as an
|
|
444
446
|
# external asset so the toolbar carries no inline style= and renders
|
|
@@ -934,6 +936,18 @@ module Tina4
|
|
|
934
936
|
end
|
|
935
937
|
end
|
|
936
938
|
|
|
939
|
+
# Detached split view — dashboard/editor LEFT, live app preview RIGHT.
|
|
940
|
+
# Served from the SHARED asset (the same tina4-dev-admin-split.html the
|
|
941
|
+
# python/php/node frameworks serve); a plain shell, no toolbar injected.
|
|
942
|
+
def serve_split
|
|
943
|
+
path = File.join(File.dirname(__FILE__), "public", "js", "tina4-dev-admin-split.html")
|
|
944
|
+
if File.file?(path)
|
|
945
|
+
[200, { "content-type" => "text/html; charset=utf-8" }, [File.read(path)]]
|
|
946
|
+
else
|
|
947
|
+
[404, { "content-type" => "text/plain" }, ["tina4-dev-admin-split.html not found"]]
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
937
951
|
def status_payload
|
|
938
952
|
db_table_count = 0
|
|
939
953
|
begin
|
data/lib/tina4/metrics.rb
CHANGED
|
@@ -50,7 +50,11 @@ module Tina4
|
|
|
50
50
|
binary = engine_path
|
|
51
51
|
raise MetricsEngineError, "tina4 not found on PATH - #{INSTALL_HINT}" unless binary
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
# --no-history: a dashboard READ must never write .tina4-metrics.json.
|
|
54
|
+
# Without it every scan records history, the file watcher sees the write
|
|
55
|
+
# and reloads the page, which re-scans - a self-inflicted ~2-second reload
|
|
56
|
+
# loop that makes the dashboard unusable.
|
|
57
|
+
stdout, stderr, status = Open3.capture3(binary, 'metrics', '--path', path.to_s, '--json', '--no-history')
|
|
54
58
|
unless status.success?
|
|
55
59
|
detail = (stderr.empty? ? stdout : stderr).strip.lines.first || "exit code #{status.exitstatus}"
|
|
56
60
|
raise MetricsEngineError, "tina4 metrics failed on #{path}: #{detail.strip}"
|
data/lib/tina4/plan.rb
CHANGED
|
@@ -407,7 +407,7 @@ module Tina4
|
|
|
407
407
|
user_parts << "Extra context from caller: #{prompt}" unless prompt.to_s.empty?
|
|
408
408
|
user_parts << "Reply with ONLY the JSON array — no explanation, no markdown fences."
|
|
409
409
|
|
|
410
|
-
ai_url = ENV.fetch("TINA4_AI_URL", "http://localhost:
|
|
410
|
+
ai_url = ENV.fetch("TINA4_AI_URL", "http://localhost:11435/api/chat")
|
|
411
411
|
ai_model = ENV.fetch("TINA4_AI_MODEL", "qwen2.5-coder:14b")
|
|
412
412
|
|
|
413
413
|
reply = begin
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"path": "js/tina4-dev-admin.min.js",
|
|
44
|
-
"component": "tina4-dev-admin-
|
|
45
|
-
"sha256": "
|
|
44
|
+
"component": "tina4-dev-admin-8c4617d",
|
|
45
|
+
"sha256": "6b05be06b5d76ee4126a11f019d104d3920c1e3234250cfe1966a33f34c1a234"
|
|
46
46
|
}
|
|
47
47
|
]
|
|
48
48
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev - Split</title>
|
|
3
|
+
<style>
|
|
4
|
+
html,body{margin:0;height:100%;background:#0f172a;font-family:system-ui,-apple-system,sans-serif;overflow:hidden}
|
|
5
|
+
/* One top toolbar controls both panels and the preview device. */
|
|
6
|
+
.t4-topbar{height:40px;box-sizing:border-box;display:flex;align-items:center;gap:6px;padding:0 10px;background:#0b1220;border-bottom:1px solid #1f2937}
|
|
7
|
+
.t4-brand{color:#e2e8f0;font:600 12px system-ui;margin-right:6px}
|
|
8
|
+
.t4-spacer{flex:1 1 auto}
|
|
9
|
+
.t4-devbtn{background:#1e293b;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:3px 9px;font:12px system-ui;cursor:pointer}
|
|
10
|
+
.t4-devbtn.active{background:#2563eb;border-color:#2563eb;color:#fff}
|
|
11
|
+
/* Icon toggle buttons: full opacity + highlight when the panel is shown,
|
|
12
|
+
dimmed when hidden. */
|
|
13
|
+
.t4-tg{display:inline-flex;align-items:center;justify-content:center;width:28px;height:26px;background:#1e293b;color:#94a3b8;border:1px solid #334155;border-radius:6px;cursor:pointer;opacity:.5}
|
|
14
|
+
.t4-tg svg{width:15px;height:15px}
|
|
15
|
+
.t4-tg.active{background:#334155;color:#fff;opacity:1}
|
|
16
|
+
.t4-dim{color:#64748b;font:11px/1 ui-monospace,monospace;margin:0 6px}
|
|
17
|
+
.t4-popout{background:#0f766e;color:#fff;border:1px solid #0f766e;border-radius:6px;padding:3px 10px;font:12px system-ui;cursor:pointer;margin-left:6px}
|
|
18
|
+
.t4-split{display:flex;height:calc(100vh - 41px);width:100vw}
|
|
19
|
+
.t4-left{flex:0 0 auto;width:58%;min-width:0;height:100%;overflow:hidden}
|
|
20
|
+
.t4-left iframe{width:100%;height:100%;border:0;display:block}
|
|
21
|
+
.t4-gutter{flex:0 0 6px;cursor:col-resize;background:#1f2937}
|
|
22
|
+
.t4-gutter:hover,.t4-gutter.drag{background:#2563eb}
|
|
23
|
+
.t4-right{flex:1 1 auto;min-width:0;height:100%;overflow:hidden;display:flex;flex-direction:column}
|
|
24
|
+
/* A LIGHT backdrop so the dark device frame stands out. */
|
|
25
|
+
.t4-stage{flex:1 1 auto;min-width:0;min-height:0;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 32%,#f8fafc,#c3ccd9)}
|
|
26
|
+
/* The device frame - a phone/tablet bezel around the live app. overflow:hidden
|
|
27
|
+
+ border-radius CLIP the app to the rounded frame, so content is contained.
|
|
28
|
+
No width/height transition: fit() measures right after a toggle, and an
|
|
29
|
+
animated size would be read mid-transition (the "clicking does nothing" bug). */
|
|
30
|
+
.t4-device{position:relative;background:#111;box-shadow:0 16px 50px rgba(15,23,42,.45);transform-origin:center center;overflow:hidden}
|
|
31
|
+
.t4-device iframe{width:100%;height:100%;border:0;display:block;background:#fff}
|
|
32
|
+
.t4-device.mobile{border:14px solid #111;border-radius:46px}
|
|
33
|
+
.t4-device.tablet{border:16px solid #111;border-radius:26px}
|
|
34
|
+
/* Desktop fills the whole preview block - no bezel, no scaling. */
|
|
35
|
+
.t4-device.desktop{border:0;border-radius:0;box-shadow:none;width:100%;height:100%}
|
|
36
|
+
.t4-hidden{display:none !important}
|
|
37
|
+
/* While dragging the gutter, let mouse events pass over the iframes. */
|
|
38
|
+
body.t4-dragging iframe{pointer-events:none}
|
|
39
|
+
</style></head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="t4-topbar">
|
|
42
|
+
<span class="t4-brand">Tina4 Dev</span>
|
|
43
|
+
<button class="t4-tg active" id="t4-tg-files" title="Show/hide the file navigator"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><line x1="6" y1="2.5" x2="6" y2="13.5"/></svg></button>
|
|
44
|
+
<button class="t4-tg active" id="t4-tg-editor" title="Show/hide the code editor"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="5.5,4.5 2,8 5.5,11.5"/><polyline points="10.5,4.5 14,8 10.5,11.5"/></svg></button>
|
|
45
|
+
<button class="t4-tg active" id="t4-tg-preview" title="Show/hide the preview"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><line x1="5.5" y1="14" x2="10.5" y2="14"/><line x1="8" y1="11.5" x2="8" y2="14"/></svg></button>
|
|
46
|
+
<span class="t4-spacer"></span>
|
|
47
|
+
<span class="t4-dim" id="t4-dim">1440 x 900</span>
|
|
48
|
+
<button class="t4-devbtn" data-mode="mobile">Mobile</button>
|
|
49
|
+
<button class="t4-devbtn" data-mode="tablet">Tablet</button>
|
|
50
|
+
<button class="t4-devbtn active" data-mode="desktop">Desktop</button>
|
|
51
|
+
<button class="t4-popout" id="t4-popout" title="Open this view in its own window - drag it to another screen">⇱ Pop out</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="t4-split" id="t4-split">
|
|
54
|
+
<div class="t4-left" id="t4-left"><iframe src="/__dev" title="Tina4 Dev Admin"></iframe></div>
|
|
55
|
+
<div class="t4-gutter" id="t4-gutter"></div>
|
|
56
|
+
<div class="t4-right" id="t4-right">
|
|
57
|
+
<div class="t4-stage" id="t4-stage">
|
|
58
|
+
<div class="t4-device desktop" id="t4-device">
|
|
59
|
+
<iframe id="t4-preview" src="/" title="Live preview"></iframe>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<script>
|
|
65
|
+
(function () {
|
|
66
|
+
var SIZES = { mobile: {w:390,h:844}, tablet: {w:820,h:1180}, desktop: {w:1440,h:900} };
|
|
67
|
+
var KEY = 'tina4_split_device';
|
|
68
|
+
var device = document.getElementById('t4-device');
|
|
69
|
+
var stage = document.getElementById('t4-stage');
|
|
70
|
+
var dim = document.getElementById('t4-dim');
|
|
71
|
+
var left = document.getElementById('t4-left');
|
|
72
|
+
var right = document.getElementById('t4-right');
|
|
73
|
+
var gutter = document.getElementById('t4-gutter');
|
|
74
|
+
var split = document.getElementById('t4-split');
|
|
75
|
+
var mode = 'desktop';
|
|
76
|
+
try { var saved = localStorage.getItem(KEY); if (saved && SIZES[saved]) mode = saved; } catch (_) {}
|
|
77
|
+
var leftWidth = '58%';
|
|
78
|
+
|
|
79
|
+
// ── device preview ──
|
|
80
|
+
function fit() {
|
|
81
|
+
if (right.classList.contains('t4-hidden')) return;
|
|
82
|
+
if (mode === 'desktop') {
|
|
83
|
+
// Desktop just fills the preview block - responsive, no frame, no scale.
|
|
84
|
+
device.style.transform = 'none';
|
|
85
|
+
device.style.width = '100%';
|
|
86
|
+
device.style.height = '100%';
|
|
87
|
+
var r0 = stage.getBoundingClientRect();
|
|
88
|
+
if (dim) dim.textContent = Math.round(r0.width) + ' x ' + Math.round(r0.height);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
var s = SIZES[mode];
|
|
92
|
+
device.style.transform = 'none';
|
|
93
|
+
device.style.width = s.w + 'px';
|
|
94
|
+
device.style.height = s.h + 'px';
|
|
95
|
+
var rect = stage.getBoundingClientRect();
|
|
96
|
+
var pad = 28;
|
|
97
|
+
var ow = device.offsetWidth, oh = device.offsetHeight;
|
|
98
|
+
var scale = Math.min((rect.width - pad) / ow, (rect.height - pad) / oh, 1);
|
|
99
|
+
if (!isFinite(scale) || scale <= 0) scale = 1;
|
|
100
|
+
device.style.transform = 'scale(' + scale + ')';
|
|
101
|
+
}
|
|
102
|
+
function setMode(m) {
|
|
103
|
+
if (!SIZES[m]) return;
|
|
104
|
+
mode = m;
|
|
105
|
+
device.className = 't4-device ' + m;
|
|
106
|
+
if (dim) dim.textContent = SIZES[m].w + ' x ' + SIZES[m].h;
|
|
107
|
+
document.querySelectorAll('.t4-devbtn').forEach(function (b) {
|
|
108
|
+
b.classList.toggle('active', b.getAttribute('data-mode') === m);
|
|
109
|
+
});
|
|
110
|
+
try { localStorage.setItem(KEY, m); } catch (_) {}
|
|
111
|
+
fit();
|
|
112
|
+
}
|
|
113
|
+
document.querySelectorAll('.t4-devbtn').forEach(function (b) {
|
|
114
|
+
b.addEventListener('click', function () { setMode(b.getAttribute('data-mode')); });
|
|
115
|
+
});
|
|
116
|
+
var pop = document.getElementById('t4-popout');
|
|
117
|
+
if (pop) pop.addEventListener('click', function () {
|
|
118
|
+
var s = SIZES[mode];
|
|
119
|
+
var w = Math.min(s.w, (screen.availWidth || 1920) - 40);
|
|
120
|
+
var h = Math.min(s.h, (screen.availHeight || 1080) - 120);
|
|
121
|
+
window.open('/', 'tina4preview_' + mode, 'width=' + w + ',height=' + h + ',menubar=no,toolbar=no,location=no');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── show / hide each panel (Files, Editor, Preview) ──
|
|
125
|
+
// Files (the nav) and Editor (the code area) are SEPARATE sections living
|
|
126
|
+
// inside the left /__dev SPA iframe, so hiding the editor must not take the
|
|
127
|
+
// file nav with it. They are same-origin, so we reach into the iframe and
|
|
128
|
+
// toggle its own `#editor-sidebar` (nav) and `.editor-main` (editor). The
|
|
129
|
+
// Preview is the right pane of this page.
|
|
130
|
+
var vis = { files: true, editor: true, preview: true };
|
|
131
|
+
function frameDoc() {
|
|
132
|
+
try { return document.querySelector('#t4-left iframe').contentDocument; } catch (_) { return null; }
|
|
133
|
+
}
|
|
134
|
+
function setFrameEl(sel, show) {
|
|
135
|
+
var d = frameDoc(); if (!d) return;
|
|
136
|
+
var el = d.querySelector(sel); if (el) el.style.display = show ? '' : 'none';
|
|
137
|
+
}
|
|
138
|
+
function applyPanels() {
|
|
139
|
+
setFrameEl('#editor-sidebar', vis.files);
|
|
140
|
+
setFrameEl('#editor-splitter-left', vis.files && vis.editor);
|
|
141
|
+
setFrameEl('.editor-main', vis.editor);
|
|
142
|
+
var leftEmpty = !vis.files && !vis.editor;
|
|
143
|
+
left.classList.toggle('t4-hidden', leftEmpty);
|
|
144
|
+
right.classList.toggle('t4-hidden', !vis.preview);
|
|
145
|
+
gutter.classList.toggle('t4-hidden', leftEmpty || !vis.preview);
|
|
146
|
+
if (!vis.preview) {
|
|
147
|
+
// Preview hidden: the editor/nav fills the whole width.
|
|
148
|
+
left.style.flex = '1 1 auto'; left.style.width = 'auto';
|
|
149
|
+
} else if (vis.files && !vis.editor) {
|
|
150
|
+
// Only the file nav is shown: shrink the left pane to the nav width so
|
|
151
|
+
// there is no empty gap where the editor used to be.
|
|
152
|
+
left.style.flex = '0 0 auto'; left.style.width = '250px';
|
|
153
|
+
} else {
|
|
154
|
+
left.style.flex = '0 0 auto'; left.style.width = leftWidth;
|
|
155
|
+
}
|
|
156
|
+
document.getElementById('t4-tg-files').classList.toggle('active', vis.files);
|
|
157
|
+
document.getElementById('t4-tg-editor').classList.toggle('active', vis.editor);
|
|
158
|
+
document.getElementById('t4-tg-preview').classList.toggle('active', vis.preview);
|
|
159
|
+
fit();
|
|
160
|
+
}
|
|
161
|
+
function toggle(name) {
|
|
162
|
+
// Never hide all three - at least one stays visible.
|
|
163
|
+
var n = { files: vis.files, editor: vis.editor, preview: vis.preview };
|
|
164
|
+
n[name] = !n[name];
|
|
165
|
+
if (!n.files && !n.editor && !n.preview) return;
|
|
166
|
+
vis[name] = !vis[name];
|
|
167
|
+
applyPanels();
|
|
168
|
+
}
|
|
169
|
+
document.getElementById('t4-tg-files').addEventListener('click', function () { toggle('files'); });
|
|
170
|
+
document.getElementById('t4-tg-editor').addEventListener('click', function () { toggle('editor'); });
|
|
171
|
+
document.getElementById('t4-tg-preview').addEventListener('click', function () { toggle('preview'); });
|
|
172
|
+
|
|
173
|
+
// ── drag the gutter to resize ──
|
|
174
|
+
var dragging = false;
|
|
175
|
+
gutter.addEventListener('mousedown', function (e) {
|
|
176
|
+
dragging = true;
|
|
177
|
+
document.body.classList.add('t4-dragging');
|
|
178
|
+
gutter.classList.add('drag');
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
});
|
|
181
|
+
window.addEventListener('mousemove', function (e) {
|
|
182
|
+
if (!dragging) return;
|
|
183
|
+
var r = split.getBoundingClientRect();
|
|
184
|
+
var w = e.clientX - r.left;
|
|
185
|
+
w = Math.max(220, Math.min(w, r.width - 260));
|
|
186
|
+
leftWidth = w + 'px';
|
|
187
|
+
left.style.width = leftWidth;
|
|
188
|
+
fit();
|
|
189
|
+
});
|
|
190
|
+
window.addEventListener('mouseup', function () {
|
|
191
|
+
if (!dragging) return;
|
|
192
|
+
dragging = false;
|
|
193
|
+
document.body.classList.remove('t4-dragging');
|
|
194
|
+
gutter.classList.remove('drag');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
window.addEventListener('resize', fit);
|
|
198
|
+
setMode(mode);
|
|
199
|
+
applyPanels();
|
|
200
|
+
})();
|
|
201
|
+
</script>
|
|
202
|
+
</body></html>
|