tina4ruby 3.13.97 → 3.13.99
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 +84 -0
- data/lib/tina4/ai.rb +32 -3
- data/lib/tina4/api.rb +5 -0
- data/lib/tina4/auto_crud.rb +62 -4
- data/lib/tina4/background.rb +112 -31
- data/lib/tina4/cache.rb +3 -2
- data/lib/tina4/cli.rb +55 -67
- data/lib/tina4/database.rb +97 -49
- data/lib/tina4/database_adapter.rb +169 -15
- data/lib/tina4/dev_admin.rb +137 -9
- data/lib/tina4/dispatch_pipeline.rb +145 -4
- data/lib/tina4/drivers/firebird_driver.rb +59 -12
- data/lib/tina4/drivers/mongodb_driver.rb +98 -14
- data/lib/tina4/drivers/mssql_driver.rb +39 -2
- data/lib/tina4/drivers/mysql_driver.rb +43 -3
- data/lib/tina4/drivers/odbc_driver.rb +36 -2
- data/lib/tina4/drivers/postgres_driver.rb +5 -0
- data/lib/tina4/drivers/sqlite_driver.rb +11 -1
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/error_overlay.rb +43 -49
- data/lib/tina4/field_types.rb +33 -16
- data/lib/tina4/frond.rb +24 -2
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +1 -1
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +1 -1
- data/lib/tina4/graphql.rb +2 -2
- data/lib/tina4/log.rb +652 -485
- data/lib/tina4/mcp.rb +9 -1
- data/lib/tina4/messenger.rb +25 -0
- data/lib/tina4/middleware.rb +189 -76
- data/lib/tina4/migration.rb +47 -15
- data/lib/tina4/orm.rb +280 -59
- data/lib/tina4/port_takeover.rb +202 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +23 -19
- data/lib/tina4/rack_app.rb +201 -59
- data/lib/tina4/realtime.rb +6 -1
- data/lib/tina4/request.rb +259 -51
- data/lib/tina4/router.rb +20 -2
- data/lib/tina4/seeder.rb +68 -19
- data/lib/tina4/shutdown.rb +4 -0
- data/lib/tina4/sql_translator.rb +115 -86
- data/lib/tina4/swagger.rb +19 -3
- data/lib/tina4/template.rb +61 -6
- data/lib/tina4/test_client.rb +49 -3
- data/lib/tina4/testing.rb +16 -11
- data/lib/tina4/validator.rb +7 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -40
- data/lib/tina4.rb +12 -1
- metadata +3 -19
- data/lib/tina4/scss/tina4css/_alerts.scss +0 -34
- data/lib/tina4/scss/tina4css/_badges.scss +0 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +0 -69
- data/lib/tina4/scss/tina4css/_cards.scss +0 -49
- data/lib/tina4/scss/tina4css/_forms.scss +0 -156
- data/lib/tina4/scss/tina4css/_grid.scss +0 -81
- data/lib/tina4/scss/tina4css/_modals.scss +0 -84
- data/lib/tina4/scss/tina4css/_nav.scss +0 -149
- data/lib/tina4/scss/tina4css/_pagination.scss +0 -63
- data/lib/tina4/scss/tina4css/_reset.scss +0 -94
- data/lib/tina4/scss/tina4css/_tables.scss +0 -54
- data/lib/tina4/scss/tina4css/_typography.scss +0 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +0 -208
- data/lib/tina4/scss/tina4css/_variables.scss +0 -117
- data/lib/tina4/scss/tina4css/base.scss +0 -1
- data/lib/tina4/scss/tina4css/colors.scss +0 -48
- data/lib/tina4/scss/tina4css/tina4.scss +0 -18
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
# Identity-checked port takeover, shared by the CLI and the runtime paths.
|
|
5
|
+
#
|
|
6
|
+
# `tina4 serve` reclaims a busy port so the edit-restart loop does not fail
|
|
7
|
+
# with "address already in use". The convenience has a sharp edge: "whatever is
|
|
8
|
+
# listening" is not always the old Tina4 server, and before this module BOTH
|
|
9
|
+
# takeover paths (the CLI #kill_process_on_port and the runtime bind-failure
|
|
10
|
+
# WebServer#free_port) SIGTERM'd whatever held the port, with NO check that the
|
|
11
|
+
# victim was a Tina4 dev server -- a foreign holder (another dev server, a
|
|
12
|
+
# database, a stray listener) was killed.
|
|
13
|
+
#
|
|
14
|
+
# This is the ONE takeover implementation both paths call (TAKEOVER-DEC-02), so
|
|
15
|
+
# the runtime path can never again be a weaker twin of the CLI path. It adds:
|
|
16
|
+
#
|
|
17
|
+
# - Identity (TAKEOVER-DEC-01): a Tina4 dev server writes a per-port PID file
|
|
18
|
+
# (`data/.tina4-serve-<port>.pid`) when it binds and removes it on clean
|
|
19
|
+
# exit. Takeover only signals a holder whose PID matches that file; a holder
|
|
20
|
+
# with no matching Tina4 PID file is REFUSED, never killed.
|
|
21
|
+
# - Dev gate + opt-out (TAKEOVER-DEC-03): takeover runs only in dev
|
|
22
|
+
# (`TINA4_DEBUG` truthy) and only when not opted out (`TINA4_NO_TAKEOVER` /
|
|
23
|
+
# `tina4 serve --no-kill`). A production bind never kills a port holder.
|
|
24
|
+
# - The existing PID safety filter and container guard, unchanged, on top.
|
|
25
|
+
#
|
|
26
|
+
# Refusing is always safe (the developer frees the port by hand); over-killing
|
|
27
|
+
# was the bug this fixes.
|
|
28
|
+
module PortTakeover
|
|
29
|
+
NOTHING = "nothing"
|
|
30
|
+
KILLED = "killed"
|
|
31
|
+
REFUSED_FOREIGN = "refused_foreign"
|
|
32
|
+
REFUSED_OPTOUT = "refused_optout"
|
|
33
|
+
REFUSED_PROD = "refused_prod"
|
|
34
|
+
SKIPPED_CONTAINER = "skipped_container"
|
|
35
|
+
REFUSALS = [REFUSED_FOREIGN, REFUSED_OPTOUT, REFUSED_PROD].freeze
|
|
36
|
+
|
|
37
|
+
# What a takeover attempt did, so each caller can react in its own idiom.
|
|
38
|
+
Result = Struct.new(:status, :port, :killed, :message) do
|
|
39
|
+
def reclaimed?
|
|
40
|
+
status == KILLED
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def refused?
|
|
44
|
+
REFUSALS.include?(status)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
def truthy?(value)
|
|
51
|
+
%w[true 1 yes on].include?(value.to_s.strip.downcase)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Dev mode = TINA4_DEBUG truthy. Takeover runs only in dev.
|
|
55
|
+
def dev?
|
|
56
|
+
truthy?(ENV["TINA4_DEBUG"])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# True when takeover is disabled via TINA4_NO_TAKEOVER.
|
|
60
|
+
def no_takeover_opted_out?
|
|
61
|
+
truthy?(ENV["TINA4_NO_TAKEOVER"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# True when this process is running inside a container. Reclaiming a port
|
|
65
|
+
# makes sense on a dev machine; inside a container the server IS the
|
|
66
|
+
# container, so there is no stale sibling to reclaim from.
|
|
67
|
+
def in_container?
|
|
68
|
+
return true if File.exist?("/.dockerenv") || File.exist?("/run/.containerenv")
|
|
69
|
+
|
|
70
|
+
blob = File.read("/proc/1/cgroup")
|
|
71
|
+
blob.include?("docker") || blob.include?("containerd") || blob.include?("kubepods")
|
|
72
|
+
rescue SystemCallError
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The PIDs from `lsof -ti` output that are safe to signal.
|
|
77
|
+
#
|
|
78
|
+
# Pure so the safety rule can be tested directly. A non-numeric field becomes
|
|
79
|
+
# 0 under `to_i`, and signalling PID 0 hits EVERY process in the caller's own
|
|
80
|
+
# process group -- the server kills itself. Accept only all-digit tokens;
|
|
81
|
+
# never PID 0 (our group), PID 1 (init), ourselves, or our own process group.
|
|
82
|
+
# This is the PID-SAFETY gate only; whether a survivor is a Tina4 server is
|
|
83
|
+
# the SEPARATE identity check in #take_over_port.
|
|
84
|
+
def selectable_pids(lsof_output, me, my_group = nil)
|
|
85
|
+
pids = []
|
|
86
|
+
lsof_output.split(/\s+/).each do |token|
|
|
87
|
+
next unless token.match?(/\A\d+\z/) # never coerce junk into a PID
|
|
88
|
+
|
|
89
|
+
pid = token.to_i
|
|
90
|
+
next if pid <= 1 || pid == me # 0 = our group, 1 = init, me = suicide
|
|
91
|
+
next if !my_group.nil? && pid == my_group
|
|
92
|
+
|
|
93
|
+
pids << pid unless pids.include?(pid)
|
|
94
|
+
end
|
|
95
|
+
pids
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def runtime_dir(base_dir = nil)
|
|
99
|
+
base_dir || File.join(Dir.pwd, "data")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pidfile_path(port, base_dir = nil)
|
|
103
|
+
File.join(runtime_dir(base_dir), ".tina4-serve-#{port}.pid")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Record THIS process as the Tina4 dev server on *port* (best-effort).
|
|
107
|
+
def write_pidfile(port, base_dir = nil, pid = nil)
|
|
108
|
+
dir = runtime_dir(base_dir)
|
|
109
|
+
require "fileutils"
|
|
110
|
+
FileUtils.mkdir_p(dir)
|
|
111
|
+
File.write(pidfile_path(port, base_dir), (pid || Process.pid).to_s)
|
|
112
|
+
rescue SystemCallError
|
|
113
|
+
nil # identity is a convenience; never let it break the server
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The PID a Tina4 dev server recorded for *port*, or nil if none/garbage.
|
|
117
|
+
def read_pidfile(port, base_dir = nil)
|
|
118
|
+
token = File.read(pidfile_path(port, base_dir)).strip
|
|
119
|
+
token.match?(/\A\d+\z/) ? token.to_i : nil
|
|
120
|
+
rescue SystemCallError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Drop the PID file for *port* (clean shutdown, or after reclaiming it).
|
|
125
|
+
def remove_pidfile(port, base_dir = nil)
|
|
126
|
+
File.delete(pidfile_path(port, base_dir))
|
|
127
|
+
rescue SystemCallError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Raw lsof/netstat PID tokens for whatever holds *port*.
|
|
132
|
+
def port_holders(port)
|
|
133
|
+
if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
134
|
+
tokens = []
|
|
135
|
+
`netstat -ano 2>&1`.each_line do |line|
|
|
136
|
+
next unless line.include?(":#{port}") &&
|
|
137
|
+
(line.include?("LISTENING") || line.include?("ESTABLISHED"))
|
|
138
|
+
|
|
139
|
+
candidate = line.strip.split(/\s+/).last
|
|
140
|
+
tokens << candidate if candidate&.match?(/\A\d+\z/)
|
|
141
|
+
end
|
|
142
|
+
tokens
|
|
143
|
+
else
|
|
144
|
+
`lsof -ti :#{port} 2>/dev/null`.split
|
|
145
|
+
end
|
|
146
|
+
rescue StandardError
|
|
147
|
+
[]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Reclaim *port* ONLY from an identity-confirmed Tina4 dev server. The single
|
|
151
|
+
# guarded path for both the CLI (`tina4 serve`) and the runtime bind-failure
|
|
152
|
+
# fallback. `dev`/`no_takeover` are passed in so this stays pure and directly
|
|
153
|
+
# testable; callers resolve them from #dev? / #no_takeover_opted_out?.
|
|
154
|
+
def take_over_port(port, dev:, no_takeover:, base_dir: nil, grace: 0.5)
|
|
155
|
+
if no_takeover
|
|
156
|
+
return Result.new(REFUSED_OPTOUT, port, [],
|
|
157
|
+
"Port #{port} is in use and takeover is disabled " \
|
|
158
|
+
"(TINA4_NO_TAKEOVER/--no-kill) -- free it or choose another port.")
|
|
159
|
+
end
|
|
160
|
+
unless dev
|
|
161
|
+
return Result.new(REFUSED_PROD, port, [],
|
|
162
|
+
"Port #{port} is in use; takeover is disabled outside dev mode " \
|
|
163
|
+
"-- free it or choose another port.")
|
|
164
|
+
end
|
|
165
|
+
return Result.new(SKIPPED_CONTAINER, port, [], "") if in_container?
|
|
166
|
+
|
|
167
|
+
tokens = port_holders(port)
|
|
168
|
+
return Result.new(NOTHING, port, [], "") if tokens.empty?
|
|
169
|
+
|
|
170
|
+
me = Process.pid
|
|
171
|
+
my_group = begin
|
|
172
|
+
Process.getpgrp
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
holders = selectable_pids(tokens.join(" "), me, my_group)
|
|
177
|
+
return Result.new(NOTHING, port, [], "") if holders.empty?
|
|
178
|
+
|
|
179
|
+
recorded = read_pidfile(port, base_dir)
|
|
180
|
+
tina4_holders = recorded.nil? ? [] : holders.select { |pid| pid == recorded }
|
|
181
|
+
if tina4_holders.empty?
|
|
182
|
+
return Result.new(REFUSED_FOREIGN, port, [],
|
|
183
|
+
"Port #{port} is held by a non-Tina4 process " \
|
|
184
|
+
"-- free it or choose another port.")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
killed = []
|
|
188
|
+
tina4_holders.each do |pid|
|
|
189
|
+
Process.kill("TERM", pid)
|
|
190
|
+
killed << pid
|
|
191
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
192
|
+
# already gone or no permission
|
|
193
|
+
end
|
|
194
|
+
return Result.new(NOTHING, port, [], "") if killed.empty?
|
|
195
|
+
|
|
196
|
+
remove_pidfile(port, base_dir)
|
|
197
|
+
sleep(grace) if grace.positive?
|
|
198
|
+
Result.new(KILLED, port, killed,
|
|
199
|
+
"Reclaimed port #{port} from Tina4 dev server (PID: #{killed.join(', ')}).")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var n2=Object.defineProperty;var r2=(Ei,L,Q)=>L in Ei?n2(Ei,L,{enumerable:!0,configurable:!0,writable:!0,value:Q}):Ei[L]=Q;var xa=(Ei,L,Q)=>r2(Ei,typeof L!="symbol"?L+"":L,Q);(function(){"use strict";const Ei="/__dev/api";async function L(i,e="GET",t){const n={method:e,headers:{}};t&&(n.headers["Content-Type"]="application/json",n.body=JSON.stringify(t));const r=await fetch(Ei+i,n),s=await r.text();let o;if(s)try{o=JSON.parse(s)}catch{o=s}if(!r.ok){const a=o&&typeof o=="object"?o.error||o.message||JSON.stringify(o):typeof o=="string"&&o?o.slice(0,200):"";throw new Error(a?`${r.status}: ${a}`:`HTTP ${r.status}`)}return o}function Q(i){const e=document.createElement("span");return e.textContent=i,e.innerHTML}const kd={python:{color:"#3b82f6",name:"Python"},php:{color:"#8b5cf6",name:"PHP"},ruby:{color:"#ef4444",name:"Ruby"},nodejs:{color:"#22c55e",name:"Node.js"}};function oy(){const i=document.getElementById("app"),e=(i==null?void 0:i.dataset.framework)??"python",t=i==null?void 0:i.dataset.color,n=kd[e]??kd.python;return{framework:e,color:t??n.color,name:n.name}}function ay(i){const e=document.documentElement;e.style.setProperty("--primary",i.color),e.style.setProperty("--bg","#0f172a"),e.style.setProperty("--surface","#1e293b"),e.style.setProperty("--border","#334155"),e.style.setProperty("--text","#e2e8f0"),e.style.setProperty("--muted","#94a3b8"),e.style.setProperty("--success","#22c55e"),e.style.setProperty("--danger","#ef4444"),e.style.setProperty("--warn","#f59e0b"),e.style.setProperty("--info","#3b82f6")}const ly=`
|
|
2
2
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3
3
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); }
|
|
4
4
|
|
|
@@ -580,7 +580,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
580
580
|
</div>
|
|
581
581
|
</div>
|
|
582
582
|
</div>
|
|
583
|
-
`,UZ(),
|
|
583
|
+
`,UZ(),Nz(),Ue(".").then(()=>ZZ()),Mi("plan_current",{}).then(e=>{$a(e.ok&&e.result||null)}),EQ(),vQ(),Sz(bz()),Pz(),GQ(),vz().catch(()=>{});try{localStorage.removeItem("tina4.editor.chatHistory.v1")}catch{}}const TQ="tina4.editor.state";function nd(){try{const i={openPaths:ee.map(e=>e.path),activeFile:B,activeDir:ss,expandedDirs:Array.from(fi)};localStorage.setItem(TQ,JSON.stringify(i))}catch{}}async function ZZ(){let i;try{i=JSON.parse(localStorage.getItem(TQ)||"{}")}catch{return}for(const e of i.expandedDirs||[])fi.add(e),Kn.has(e)||await Ue(e);i.activeDir&&(ss=i.activeDir);for(const e of i.openPaths||[])try{await ui(e)}catch{}i.activeFile&&ee.some(e=>e.path===i.activeFile)&&ls(i.activeFile),os()}async function Ue(i){try{const e=await L(`/files?path=${encodeURIComponent(i)}`);if(e.branch){const t=document.getElementById("editor-branch");t&&(t.textContent=`⎇ ${e.branch}`)}Kn.set(i,e.entries||[]),os()}catch(e){console.error("Failed to load file tree:",e)}}function os(){const i=document.getElementById("editor-file-tree");i&&(i.innerHTML=XQ(".",0))}async function zZ(){const i=document.getElementById("editor-file-tree");if(!i)return;const e=Array.from(new Set([".",...Array.from(Kn.keys()),...Array.from(fi)])),t=await Promise.all(e.map(async o=>{try{const a=await L(`/files?path=${encodeURIComponent(o)}`);if(a!=null&&a.branch){const l=document.getElementById("editor-branch");l&&(l.textContent=`⎇ ${a.branch}`)}return[o,(a==null?void 0:a.entries)||[]]}catch{return null}})),n=new Map;for(const o of t){if(!o)continue;const[a,l]=o;Kn.set(a,l);for(const O of l)n.set(O.path,O.git_status||"clean")}const r=["git-clean","git-untracked","git-modified","git-added","git-deleted"],s=i.querySelectorAll(".tree-item[data-path]");for(const o of Array.from(s)){const a=o.dataset.path;if(!a)continue;const l=n.get(a);l!==void 0&&(o.classList.remove(...r),o.classList.add(`git-${l}`),qZ(o,l))}}function qZ(i,e){const n={untracked:"U",modified:"M",added:"A",deleted:"D"}[e];let r=i.querySelector(".tree-git-dot");if(!n){r&&r.remove();return}r||(r=document.createElement("span"),r.className="tree-git-dot",i.appendChild(r)),r.title=e,r.textContent=n}let rd=0,as=null;function vQ(){const e=`${location.protocol==="https:"?"wss":"ws"}://${location.host}/__dev_reload`;try{as=new WebSocket(e),as.addEventListener("message",t=>{try{const n=typeof t.data=="string"?JSON.parse(t.data):null;n&&(n.type==="reload"||n.type==="change")&&od()}catch{od()}}),as.addEventListener("close",()=>{as=null,setTimeout(()=>vQ(),5e3)})}catch{as=null}setInterval(async()=>{try{const t=await fetch("/__dev/api/mtime");if(!t.ok)return;const n=await t.json(),r=typeof n.mtime=="number"?n.mtime:0;r>rd&&(rd>0&&od(),rd=r)}catch{}},3e3)}let sd=null;function od(){sd===null&&(sd=window.setTimeout(async()=>{sd=null,await zZ().catch(()=>{})},300))}function XQ(i,e){const t=Kn.get(i);if(!t)return"";fi.has(i);let n="";const r=[...t].sort((s,o)=>s.is_dir!==o.is_dir?s.is_dir?-1:1:s.name.localeCompare(o.name));for(const s of r){const o=`git-${s.git_status||"clean"}`,a=e*16,l=Q(s.path);if(s.is_dir){const O=fi.has(s.path),d=s.has_children!==!1?O?"▾":"▸":" ",h=_Q(s.git_status),f=ss===s.path;n+=`<div class="tree-item tree-dir ${o} ${f?"active":""}" style="padding-left:${a}px"
|
|
584
584
|
data-path="${l}"
|
|
585
585
|
onclick="window.__editorToggleDir('${l}')"
|
|
586
586
|
oncontextmenu="event.preventDefault();window.__editorCtxMenu(event,'${l}',true)">
|
|
@@ -972,7 +972,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
972
972
|
<span style="opacity:0.6">${t}/${n}${s?` · ${s}%`:""}</span>
|
|
973
973
|
</span>
|
|
974
974
|
<button class="btn btn-sm" onclick="window.__editorPlanRun('${Q(i.current)}')" ${o?"disabled":""} title="Hand this plan to the Rust supervisor and let it execute remaining steps" style="font-size:0.65rem;padding:1px 6px;line-height:1;${o?"opacity:0.35;cursor:not-allowed":""}">▶</button>
|
|
975
|
-
</span>`}let mi=null;async function LZ(i){mi==null||mi.abort(),mi=new AbortController;const e=document.getElementById("editor-ai-messages");if(!e)return;const t=document.createElement("div");t.className="ai-msg ai-bot",t.style.cssText="padding:0.4rem 0.6rem;font-size:0.72rem;border-left:2px solid var(--info,#89b4fa);background:rgba(137,180,250,0.08)",t.innerHTML=`▶ <strong>Executing plan</strong> <code>plan/${Q(i)}</code> via Rust supervisor… <button class="btn btn-sm" onclick="window.__editorPlanStop()" style="font-size:0.6rem;padding:1px 6px;margin-left:0.5rem">Stop</button>`,e.appendChild(t);const n=document.createElement("div");n.className="ai-msg ai-bot",n.style.cssText="padding:0.35rem 0.6rem;font-size:0.7rem;opacity:0.8;font-family:var(--font-mono,monospace)",n.textContent="starting…",e.appendChild(n);const r=[];try{await VS(`plan/${i}`,o=>{var a;if(o.event==="status")n.textContent=`[${o.agent||"…"}] ${o.text||""}`;else if(o.event==="message"){const l=document.createElement("div");l.className="ai-msg ai-bot",l.style.cssText="padding:0.45rem 0.6rem;font-size:0.75rem";const O=(o.content||"").replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\n/g,"<br>");l.innerHTML=O,e.appendChild(l),(a=o.files_changed)!=null&&a.length&&r.push(...o.files_changed)}else o.event==="error"&&(n.textContent=`✗ ${o.text||"agent error"}`,n.style.color="var(--danger,#f38ba8)");e.scrollTop=e.scrollHeight},{signal:mi.signal}),n.textContent=r.length?`✓ done — ${r.length} file${r.length===1?"":"s"} touched`:"✓ done",n.style.color="var(--success,#a6e3a1)",await Ue(".");const s=await Mi("plan_current",{});$a(s.ok&&s.result||null)}catch(s){(s==null?void 0:s.name)==="AbortError"?n.textContent="⏹ stopped by user":(n.textContent=`✗ ${(s==null?void 0:s.message)||s}`,n.style.color="var(--danger,#f38ba8)")}finally{mi=null}}function IZ(){mi==null||mi.abort()}const CQ="tina4.editor.dismissedThoughts";function YQ(){try{const i=localStorage.getItem(CQ);if(!i)return new Set;const e=JSON.parse(i);return new Set(Array.isArray(e)?e:[])}catch{return new Set}}function GZ(i){const e=Array.from(i).slice(-200);try{localStorage.setItem(CQ,JSON.stringify(e))}catch{}}const DZ=new Set(["hey","hi","hello","there","team","folks","everyone","i","we","you","they","our","us","my","your","their","noticed","notice","saw","see","spotted","found","thought","that","this","these","those","it","its","the","a","an","and","or","but","for","of","to","in","on","at","with","as","by","from","have","has","had","having","is","are","was","were","be","been","being","do","does","did","does","done","can","could","should","would","will","may","might","must","so","just","even","still","also","too","not","no","yes","project","projects","file","files","new","old","make","makes","made","making","know","knows","knew","known","tricky","bit","exactly","some","any","all","each","every","what","which","why","how","when","where","who","if","then","else","than","because","set","up","down","over","under","out","into","onto","developer","developers","dev","devs","user","users","environment","env"]);function WQ(i){const e=(i||"").toLowerCase().replace(/[^a-z0-9._/-]+/g," ").split(/\s+/).map(s=>s.replace(/^[._\-/]+|[._\-/]+$/g,"")).filter(s=>s.length>1),t=e.filter(s=>s.includes(".")||s.includes("/"));if(t.length)return Array.from(new Set(t)).sort().slice(0,6).join("|");const n=e.filter(s=>s.length>2&&!DZ.has(s));return Array.from(new Set(n)).sort().slice(0,4).join("|")}async function EQ(){const i=document.getElementById("editor-thoughts-banner"),e=document.getElementById("thoughts-empty-state"),t=document.getElementById("thoughts-toolbar"),n=document.getElementById("thoughts-toolbar-count");if(!i)return;let r=[];try{r=await CS()}catch{r=[]}const s=YQ(),o=new Set,a=[];for(const l of r){const O=WQ(l.message);s.has(O)||o.has(O)||(o.add(O),a.push(l))}if(
|
|
975
|
+
</span>`}let mi=null;async function LZ(i){mi==null||mi.abort(),mi=new AbortController;const e=document.getElementById("editor-ai-messages");if(!e)return;const t=document.createElement("div");t.className="ai-msg ai-bot",t.style.cssText="padding:0.4rem 0.6rem;font-size:0.72rem;border-left:2px solid var(--info,#89b4fa);background:rgba(137,180,250,0.08)",t.innerHTML=`▶ <strong>Executing plan</strong> <code>plan/${Q(i)}</code> via Rust supervisor… <button class="btn btn-sm" onclick="window.__editorPlanStop()" style="font-size:0.6rem;padding:1px 6px;margin-left:0.5rem">Stop</button>`,e.appendChild(t);const n=document.createElement("div");n.className="ai-msg ai-bot",n.style.cssText="padding:0.35rem 0.6rem;font-size:0.7rem;opacity:0.8;font-family:var(--font-mono,monospace)",n.textContent="starting…",e.appendChild(n);const r=[];try{await VS(`plan/${i}`,o=>{var a;if(o.event==="status")n.textContent=`[${o.agent||"…"}] ${o.text||""}`;else if(o.event==="message"){const l=document.createElement("div");l.className="ai-msg ai-bot",l.style.cssText="padding:0.45rem 0.6rem;font-size:0.75rem";const O=(o.content||"").replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\n/g,"<br>");l.innerHTML=O,e.appendChild(l),(a=o.files_changed)!=null&&a.length&&r.push(...o.files_changed)}else o.event==="error"&&(n.textContent=`✗ ${o.text||"agent error"}`,n.style.color="var(--danger,#f38ba8)");e.scrollTop=e.scrollHeight},{signal:mi.signal}),n.textContent=r.length?`✓ done — ${r.length} file${r.length===1?"":"s"} touched`:"✓ done",n.style.color="var(--success,#a6e3a1)",await Ue(".");const s=await Mi("plan_current",{});$a(s.ok&&s.result||null)}catch(s){(s==null?void 0:s.name)==="AbortError"?n.textContent="⏹ stopped by user":(n.textContent=`✗ ${(s==null?void 0:s.message)||s}`,n.style.color="var(--danger,#f38ba8)")}finally{mi=null}}function IZ(){mi==null||mi.abort()}const CQ="tina4.editor.dismissedThoughts";function YQ(){try{const i=localStorage.getItem(CQ);if(!i)return new Set;const e=JSON.parse(i);return new Set(Array.isArray(e)?e:[])}catch{return new Set}}function GZ(i){const e=Array.from(i).slice(-200);try{localStorage.setItem(CQ,JSON.stringify(e))}catch{}}const DZ=new Set(["hey","hi","hello","there","team","folks","everyone","i","we","you","they","our","us","my","your","their","noticed","notice","saw","see","spotted","found","thought","that","this","these","those","it","its","the","a","an","and","or","but","for","of","to","in","on","at","with","as","by","from","have","has","had","having","is","are","was","were","be","been","being","do","does","did","does","done","can","could","should","would","will","may","might","must","so","just","even","still","also","too","not","no","yes","project","projects","file","files","new","old","make","makes","made","making","know","knows","knew","known","tricky","bit","exactly","some","any","all","each","every","what","which","why","how","when","where","who","if","then","else","than","because","set","up","down","over","under","out","into","onto","developer","developers","dev","devs","user","users","environment","env"]);function WQ(i){const e=(i||"").toLowerCase().replace(/[^a-z0-9._/-]+/g," ").split(/\s+/).map(s=>s.replace(/^[._\-/]+|[._\-/]+$/g,"")).filter(s=>s.length>1),t=e.filter(s=>s.includes(".")||s.includes("/"));if(t.length)return Array.from(new Set(t)).sort().slice(0,6).join("|");const n=e.filter(s=>s.length>2&&!DZ.has(s));return Array.from(new Set(n)).sort().slice(0,4).join("|")}async function EQ(){const i=document.getElementById("editor-thoughts-banner"),e=document.getElementById("thoughts-empty-state"),t=document.getElementById("thoughts-toolbar"),n=document.getElementById("thoughts-toolbar-count");if(!i)return;let r=[];try{r=await CS()}catch{r=[]}const s=YQ(),o=new Set,a=[];for(const l of r){const O=WQ(l.message);s.has(O)||o.has(O)||(o.add(O),a.push(l))}if(wz("thoughts",a.length||null,a.length>0),!a.length){i.style.display="none",i.innerHTML="",e&&(e.style.display=""),t&&t.setAttribute("hidden","");return}e&&(e.style.display="none"),t&&(t.removeAttribute("hidden"),n&&(n.textContent=`${a.length} observation${a.length===1?"":"s"}`)),i.style.display="flex",i.innerHTML=a.slice(0,20).map(l=>{const O=(l.message||"").slice(0,180);return`
|
|
976
976
|
<div class="editor-thought-chip" data-id="${Q(l.id)}" data-hash="${Q(WQ(l.message))}">
|
|
977
977
|
<span style="opacity:0.7">💡</span>
|
|
978
978
|
<div class="editor-thought-chip-body" onclick="window.__editorThoughtAct('${Q(l.id)}')">${Q(O)}${l.message.length>180?"…":""}</div>
|
|
@@ -1012,28 +1012,32 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
1012
1012
|
<span>severity: ${Q(e.severity||"-")}</span>
|
|
1013
1013
|
</div>
|
|
1014
1014
|
<div style="margin-top:0.3rem">${Q(e.summary||"")}</div>
|
|
1015
|
-
</div>`}catch{return fs(i)}}async function cz(){try{const i=await cs();Pe.push(i),nt.set(i.id,[]),await Sa(i.id)}catch(i){console.error("threadsNew failed",i)}}async function dz(){if(!Re)return;const i=Pe.find(t=>t.id===Re);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await Qa(Re,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function hz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function $d(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Pe.push(a),nt.set(a.id,[]),await Sa(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&Re&&hz(Pe.find(o=>o.id===Re)))try{const o=await cs(e.slice(0,80)||void 0);Pe.push(o),nt.set(o.id,[]),await Sa(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!Re)try{const o=await cs(e.slice(0,80));Pe.push(o),nt.set(o.id,[]),Re=o.id}catch(o){console.error("auto-create failed",o);return}const n=Re,r=nt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),nt.set(n,r),md(n),ga.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await
|
|
1015
|
+
</div>`}catch{return fs(i)}}async function cz(){try{const i=await cs();Pe.push(i),nt.set(i.id,[]),await Sa(i.id)}catch(i){console.error("threadsNew failed",i)}}async function dz(){if(!Re)return;const i=Pe.find(t=>t.id===Re);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await Qa(Re,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function hz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function $d(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Pe.push(a),nt.set(a.id,[]),await Sa(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&Re&&hz(Pe.find(o=>o.id===Re)))try{const o=await cs(e.slice(0,80)||void 0);Pe.push(o),nt.set(o.id,[]),await Sa(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!Re)try{const o=await cs(e.slice(0,80));Pe.push(o),nt.set(o.id,[]),Re=o.id}catch(o){console.error("auto-create failed",o);return}const n=Re,r=nt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),nt.set(n,r),md(n),ga.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await Qz(e,n,Jn.signal,s)}catch(o){if((o==null?void 0:o.name)!=="AbortError"){const a=document.createElement("div");a.className="ai-msg ai-bot",a.style.color="var(--danger,#f38ba8)",a.textContent=`Connection failed: ${(o==null?void 0:o.message)||o}`,s==null||s.appendChild(a)}}finally{ga.delete(n),Jn=null;try{await jQ(n,!0)}catch{}await ya(),md(n);const o=Pe.find(l=>l.id===n),a=document.getElementById("threads-detail-meta");if(o&&a){const l=o.sender?`<span>📨 from ${Q(o.sender)}</span>`:"";a.innerHTML=`${fd(o.status_hint||"idle")} <span>${Q(ud(o.last_message_at))}</span> ${l}`}}}queueMicrotask(()=>{const i=document.getElementById("threads-reply-form");i&&i.addEventListener("submit",t=>{t.preventDefault(),$d()});const e=document.getElementById("threads-reply-input");e&&e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),t.stopPropagation(),$d())})}),window.__threadsShowList=()=>ba(),window.__threadsShowDetail=i=>{Sa(i)},window.__threadsNew=()=>{cz()},window.__threadsRetry=()=>{ya(2).then(er)},window.__threadsRenameActive=()=>{dz()};let un=!1;async function LQ(){const i=document.getElementById("plans-panel"),e=document.getElementById("plans-toggle-btn");i&&(un=!un,i.hidden=!un,e==null||e.classList.toggle("active",un),un&&await fz())}async function fz(){const i=document.getElementById("plans-rows");if(i){i.innerHTML='<div class="threads-empty">Loading plans…</div>';try{const e=await Mi("plan_list",{}),t=e.ok&&Array.isArray(e.result)?e.result:[];if(!t.length){i.innerHTML='<div class="threads-empty">No plans yet — they appear here when the planner agent creates one.</div>';return}const n=[...t].sort((r,s)=>String(s.name||s.file||"").localeCompare(String(r.name||r.file||"")));i.innerHTML=n.map(r=>{var h,f;const s=String(r.name||r.file||""),o=String(r.path||`plan/${s}`),a=String(r.title||s).slice(0,60),l=r.steps_done??((h=r.progress)==null?void 0:h.done)??0,O=r.steps_total??((f=r.progress)==null?void 0:f.total)??0,c=O>0?`${l}/${O} steps`:"",d=r.is_current||r.current?" · ★ current":"";return`<div class="plan-row" onclick="window.__plansOpen('${Q(o)}')" title="Open ${Q(o)} in editor">
|
|
1016
1016
|
<div class="plan-name">${Q(a)}</div>
|
|
1017
1017
|
<div class="plan-meta">${Q(s)}${c?" · "+Q(c):""}${d}</div>
|
|
1018
|
-
</div>`}).join("")}catch{i.innerHTML='<div class="threads-empty" style="color:var(--danger,#f38ba8)">Failed to load plans</div>'}}}async function uz(i){var t;try{await ui(i)}catch(n){console.error("plansOpen failed",n)}un=!1;const e=document.getElementById("plans-panel");e&&(e.hidden=!0),(t=document.getElementById("plans-toggle-btn"))==null||t.classList.remove("active")}window.__plansToggle=()=>{LQ()},window.__plansOpen=i=>{uz(i)};let ds=!1;async function pz(){const i=document.getElementById("grounding-panel"),e=document.getElementById("grounding-toggle-btn");i&&(un&&LQ(),ds=!ds,i.hidden=!ds,e==null||e.classList.toggle("active",ds),ds&&await IQ())}
|
|
1018
|
+
</div>`}).join("")}catch{i.innerHTML='<div class="threads-empty" style="color:var(--danger,#f38ba8)">Failed to load plans</div>'}}}async function uz(i){var t;try{await ui(i)}catch(n){console.error("plansOpen failed",n)}un=!1;const e=document.getElementById("plans-panel");e&&(e.hidden=!0),(t=document.getElementById("plans-toggle-btn"))==null||t.classList.remove("active")}window.__plansToggle=()=>{LQ()},window.__plansOpen=i=>{uz(i)};let ds=!1;async function pz(){const i=document.getElementById("grounding-panel"),e=document.getElementById("grounding-toggle-btn");i&&(un&&LQ(),ds=!ds,i.hidden=!ds,e==null||e.classList.toggle("active",ds),ds&&await IQ())}function mz(i){const e=Q(i.url||"https://mcp.tina4.com"),t=i.source||(i.configured?"personal":"none");if(t==="personal")return{source:t,stateHtml:`<span style="color:var(--success,#a6e3a1)">● Your token</span> <span style="opacity:0.6">(…${Q(i.last4||"")})</span>`,nudgeHtml:""};if(t==="free"){const n=i.dev_email?` Identified to the server as <code>${Q(i.dev_email)}</code>.`:"";return{source:t,stateHtml:`<span style="color:var(--warn,#f9e2af)">🎁 Free trial</span> — grounding via <code>${e}</code> on the shared <code>FREE-TOKEN</code>.`,nudgeHtml:`<div style="margin:0.4rem 0;padding:0.4rem 0.55rem;border:1px solid var(--warn,#f9e2af);border-radius:6px;background:color-mix(in srgb, var(--warn,#f9e2af) 12%, transparent)">
|
|
1019
|
+
You're trying Tina4 grounding for free.${n} Register for your <strong>own</strong> token — higher limits, no shared rate cap.
|
|
1020
|
+
<a href="https://profile.tina4.com" target="_blank" rel="noopener" style="color:var(--accent,#89b4fa);font-weight:600">Register at profile.tina4.com →</a>
|
|
1021
|
+
</div>`}}return{source:t,stateHtml:'<span style="color:var(--warn,#f9e2af)">○ Not set</span> — using local corpus fallback',nudgeHtml:""}}async function IQ(){const i=document.getElementById("grounding-body");if(!i)return;i.innerHTML='<div class="threads-empty">Loading…</div>';let e={};try{const s=await fetch("/__dev/api/grounding/status");s.ok&&(e=await s.json())}catch{}const t=Q(e.url||"https://mcp.tina4.com"),{stateHtml:n,nudgeHtml:r}=mz(e);i.innerHTML=`
|
|
1019
1022
|
<div style="font-weight:600;margin-bottom:0.35rem">Framework grounding</div>
|
|
1020
1023
|
<div style="opacity:0.85;margin-bottom:0.5rem">Ground the coder against <code>${t}</code> (version-current Tina4 API) instead of the local fallback.</div>
|
|
1021
1024
|
<div style="margin-bottom:0.5rem">${n}</div>
|
|
1025
|
+
${r}
|
|
1022
1026
|
<div style="display:flex;gap:4px">
|
|
1023
|
-
<input type="password" id="grounding-token-input" class="input" placeholder="Paste TINA4_MCP_TOKEN…"
|
|
1027
|
+
<input type="password" id="grounding-token-input" class="input" placeholder="Paste your own TINA4_MCP_TOKEN…"
|
|
1024
1028
|
style="flex:1;font-size:0.72rem;padding:4px 8px;height:28px" autocomplete="off" />
|
|
1025
1029
|
<button type="button" class="btn btn-sm btn-primary" style="font-size:0.65rem;padding:2px 10px"
|
|
1026
1030
|
onclick="window.__groundingSave()">Save</button>
|
|
1027
1031
|
</div>
|
|
1028
1032
|
<div id="grounding-result" style="margin-top:0.4rem;min-height:1.1em"></div>
|
|
1029
1033
|
<div style="opacity:0.6;margin-top:0.4rem">Get a free token at <code>profile.tina4.com</code>. Stored in project <code>.env</code>; takes effect next turn.</div>
|
|
1030
|
-
`}async function
|
|
1034
|
+
`}async function $z(){const i=document.getElementById("grounding-token-input"),e=document.getElementById("grounding-result"),t=i==null?void 0:i.value.trim();if(!t){e&&(e.innerHTML='<span style="color:var(--warn,#f9e2af)">Paste a token first.</span>');return}e&&(e.innerHTML='<span style="opacity:0.7">Saving…</span>');try{const n=await fetch("/__dev/api/grounding/token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})}),r=await n.json().catch(()=>({}));n.ok&&r.ok?(i&&(i.value=""),e&&(e.innerHTML=`<span style="color:var(--success,#a6e3a1)">✓ Saved (…${Q(String(r.last4||""))}). Grounding now uses mcp.tina4.com.</span>`),setTimeout(()=>{IQ()},1200)):e&&(e.innerHTML=`<span style="color:var(--danger,#f38ba8)">Failed: ${Q(String(r.error||n.status))}</span>`)}catch{e&&(e.innerHTML='<span style="color:var(--danger,#f38ba8)">Agent unreachable — is <code>tina4 serve</code> running?</span>')}}window.__groundingToggle=()=>{pz()},window.__groundingSave=()=>{$z()};async function gz(i){const e=document.getElementById("threads-chat")||document.getElementById("editor-ai-messages"),t=document.createElement("div");t.className="ai-msg ai-bot";const n=document.createElement("div");n.style.cssText="font-size:0.72rem;opacity:0.85;font-family:var(--font-mono,monospace)",n.textContent="▶ Building…",t.appendChild(n),e==null||e.appendChild(t);const r=i.includes("/")?i:`plan/${i}`;try{const s=await fetch("/__dev/api/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({plan_file:r})});if(!s.ok||!s.body){n.textContent=`✗ execute ${s.status}`;return}const o=s.body.getReader(),a=new TextDecoder;let l="";for(;;){const{done:O,value:c}=await o.read();if(O)break;l+=a.decode(c,{stream:!0});let d;for(;(d=l.indexOf(`
|
|
1031
1035
|
|
|
1032
1036
|
`))!==-1;){const h=l.slice(0,d);l=l.slice(d+2);let f="message",p="";for(const m of h.split(`
|
|
1033
|
-
`))m.startsWith("event:")?f=m.slice(6).trim():m.startsWith("data:")&&(p+=m.slice(5).trim());if(p){try{const m=JSON.parse(p);if(f==="status")n.textContent=`[${m.agent||"…"}] ${m.text||""}`;else if(f==="message"&&m.content){const $=document.createElement("div");$.style.cssText="font-size:0.75rem;margin-top:0.25rem",$.innerHTML=Q(String(m.content)).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\\n|\n/g,"<br>"),t.appendChild($)}}catch{}e&&(e.scrollTop=e.scrollHeight)}}}n.textContent="✓ Build complete — check the file tree / run the app"}catch(s){n.textContent=`✗ ${(s==null?void 0:s.message)||s}`}}window.__buildPlanNow=i=>{
|
|
1037
|
+
`))m.startsWith("event:")?f=m.slice(6).trim():m.startsWith("data:")&&(p+=m.slice(5).trim());if(p){try{const m=JSON.parse(p);if(f==="status")n.textContent=`[${m.agent||"…"}] ${m.text||""}`;else if(f==="message"&&m.content){const $=document.createElement("div");$.style.cssText="font-size:0.75rem;margin-top:0.25rem",$.innerHTML=Q(String(m.content)).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\\n|\n/g,"<br>"),t.appendChild($)}}catch{}e&&(e.scrollTop=e.scrollHeight)}}}n.textContent="✓ Build complete — check the file tree / run the app"}catch(s){n.textContent=`✗ ${(s==null?void 0:s.message)||s}`}}window.__buildPlanNow=i=>{gz(i)},window.__threadsArchiveActive=()=>{sz()},window.__threadsArchiveFromList=i=>{oz(i)},queueMicrotask(()=>{nz()});async function Qz(i,e,t,n){const r=n??document.getElementById("threads-chat")??document.getElementById("editor-ai-messages");if(!r)return;const s=document.createElement("div");s.className="ai-msg ai-bot";const o=document.createElement("div");o.style.cssText="font-size:0.72rem;opacity:0.7;margin-bottom:0.3rem;font-family:var(--font-mono,monospace)",o.innerHTML='<span style="opacity:0.6">→ supervisor: thinking…</span>';const a=document.createElement("div");s.appendChild(o),s.appendChild(a),r.appendChild(s),r.scrollTop=r.scrollHeight;const l=(()=>{if(!B)return null;const p=ee.find($=>$.path===B);if(!p)return null;const m=p.content||"";return m.length>6e4?{path:p.path,language:p.language,content:"<too large to inline — ask the user or call file_read>"}:{path:p.path,language:p.language,content:m}})(),O=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:i,thread_id:e,active_file:l}),signal:t});if(!O.ok){let p="";try{p=(await O.text()).slice(0,400)}catch{}o.remove(),a.innerHTML=`<span style="color:var(--danger,#f38ba8)">Supervisor unavailable (HTTP ${O.status}).</span>`+(p?`<pre style="font-size:0.7rem;opacity:0.7;margin-top:0.3rem;white-space:pre-wrap">${Q(p)}</pre>`:"")+'<div style="font-size:0.75rem;opacity:0.7;margin-top:0.3rem">Run <code>tina4 serve</code> (auto-spawns the agent) or set <code>TINA4_SUPERVISOR_URL</code>.</div>';return}if(!O.body){o.remove(),a.innerHTML='<span style="color:var(--danger,#f38ba8)">Supervisor returned no body.</span>';return}const c=O.body.getReader(),d=new TextDecoder;let h="",f=[];for(;;){const{value:p,done:m}=await c.read();if(m)break;h+=d.decode(p,{stream:!0});let $;for(;($=h.indexOf(`
|
|
1034
1038
|
|
|
1035
1039
|
`))!==-1;){const g=h.slice(0,$);h=h.slice($+2);let y="message",S="";for(const w of g.split(`
|
|
1036
|
-
`))w.startsWith("event:")?y=w.slice(6).trim():w.startsWith("data:")&&(S+=w.slice(5).trim());if(!S)continue;let x;try{x=JSON.parse(S)}catch{x={text:S,content:S}}if(y==="status"){const w=String(x.agent||"supervisor"),R=String(x.text||"");o.innerHTML=`<span style="color:var(--info,#89b4fa)">[${Q(w)}]</span> ${Q(R)}`,x.backup&&(o.innerHTML+=` <span style="opacity:0.5">(backup: ${Q(String(x.backup))})</span>`)}else if(y==="message"){const w=String(x.agent||"supervisor"),R=String(x.content||"");o.innerHTML=`<span style="opacity:0.6">↳ ${Q(w)}</span>`,a.innerHTML=fs(R);const _=Array.isArray(x.suggested_replies)?x.suggested_replies.map(A=>String(A)).filter(Boolean):[];s.querySelectorAll(".action-pills").forEach(A=>A.remove()),_.length&&pd(s,_),Array.isArray(x.files_changed)&&(f=x.files_changed.map(A=>String(A)))}else if(y==="plan"){const w=String(x.content||""),R=String(x.file||"");if(o.innerHTML=`<span style="opacity:0.6">↳ planner</span> · plan saved to <code>${Q(R)}</code>`,a.innerHTML=fs(w),x.approve!==!1){if(s.querySelectorAll(".action-pills").forEach(_=>_.remove()),R){const _=document.createElement("div");_.className="action-pills";const A=document.createElement("button");A.type="button",A.className="action-pill",A.style.cssText="background:rgba(166,227,161,0.16);border-color:#a6e3a1;font-weight:600",A.textContent="⚡ Build it now",A.addEventListener("click",()=>{A.setAttribute("disabled",""),window.__buildPlanNow(R)}),_.appendChild(A),s.appendChild(_)}pd(s,["Make changes","Cancel"])}}else if(y==="error"){const w=String(x.message||"Supervisor error");o.innerHTML='<span style="color:var(--danger,#f38ba8)">✗ error</span>',a.innerHTML=`<span style="color:var(--danger,#f38ba8)">${Q(w)}</span>`}r.scrollTop=r.scrollHeight}}for(const p of f)try{await
|
|
1040
|
+
`))w.startsWith("event:")?y=w.slice(6).trim():w.startsWith("data:")&&(S+=w.slice(5).trim());if(!S)continue;let x;try{x=JSON.parse(S)}catch{x={text:S,content:S}}if(y==="status"){const w=String(x.agent||"supervisor"),R=String(x.text||"");o.innerHTML=`<span style="color:var(--info,#89b4fa)">[${Q(w)}]</span> ${Q(R)}`,x.backup&&(o.innerHTML+=` <span style="opacity:0.5">(backup: ${Q(String(x.backup))})</span>`)}else if(y==="message"){const w=String(x.agent||"supervisor"),R=String(x.content||"");o.innerHTML=`<span style="opacity:0.6">↳ ${Q(w)}</span>`,a.innerHTML=fs(R);const _=Array.isArray(x.suggested_replies)?x.suggested_replies.map(A=>String(A)).filter(Boolean):[];s.querySelectorAll(".action-pills").forEach(A=>A.remove()),_.length&&pd(s,_),Array.isArray(x.files_changed)&&(f=x.files_changed.map(A=>String(A)))}else if(y==="plan"){const w=String(x.content||""),R=String(x.file||"");if(o.innerHTML=`<span style="opacity:0.6">↳ planner</span> · plan saved to <code>${Q(R)}</code>`,a.innerHTML=fs(w),x.approve!==!1){if(s.querySelectorAll(".action-pills").forEach(_=>_.remove()),R){const _=document.createElement("div");_.className="action-pills";const A=document.createElement("button");A.type="button",A.className="action-pill",A.style.cssText="background:rgba(166,227,161,0.16);border-color:#a6e3a1;font-weight:600",A.textContent="⚡ Build it now",A.addEventListener("click",()=>{A.setAttribute("disabled",""),window.__buildPlanNow(R)}),_.appendChild(A),s.appendChild(_)}pd(s,["Make changes","Cancel"])}}else if(y==="error"){const w=String(x.message||"Supervisor error");o.innerHTML='<span style="color:var(--danger,#f38ba8)">✗ error</span>',a.innerHTML=`<span style="color:var(--danger,#f38ba8)">${Q(w)}</span>`}r.scrollTop=r.scrollHeight}}for(const p of f)try{await Vz(p)}catch{}}const yz="tina4.editor.session.mode";function bz(){return localStorage.getItem(yz)==="qa"?"qa":"supervisor"}function Sz(i){const e=document.getElementById("session-mode-toggle");if(!e)return;e.querySelectorAll(".mode-btn").forEach(n=>{n.classList.toggle("active",n.dataset.mode===i)});const t=document.getElementById("editor-ai-input");t&&(t.placeholder=i==="qa"?"Ask a question…":"Describe the change…")}function wz(i,e,t=!1){const n=document.getElementById(`tab-badge-${i}`),r=document.querySelector(`.session-tab[data-tab="${i}"]`);if(n){if(!e){n.textContent="",r==null||r.classList.remove("has-alert");return}n.textContent=String(e),r==null||r.classList.toggle("has-alert",t)}}function GQ(){const i=document.getElementById("completion-toggle");if(i){if(i.classList.remove("disabled","on-plan","off-plan"),!VQ){i.classList.add("disabled"),i.setAttribute("title","Completion off — click to enable");return}Os?(i.classList.add("on-plan"),i.setAttribute("title",`Completion on-plan: ${Os.slice(0,80)}`)):(i.classList.add("off-plan"),i.setAttribute("title","Completion on (off-plan — no intent boost)"))}}let DQ=null;async function BQ(){const i=[{key:"chat",url:tr.chat.endpoint},{key:"vision",url:tr.vision.endpoint},{key:"embed",url:tr.embed.endpoint},{key:"image",url:tr.image.endpoint},{key:"rag",url:tr.rag.endpoint}];await Promise.all(i.map(async({key:e,url:t})=>{const n=await Wy(t),r=document.querySelector(`.model-dot[data-model="${e}"]`);r&&(r.classList.toggle("up",n),r.classList.toggle("down",!n))}))}function Pz(){DQ||(BQ(),DQ=window.setInterval(BQ,3e4))}function NQ(i,e){const t=document.getElementById("session-title"),n=document.getElementById("session-meta");t&&i!==null&&(t.textContent=i),n&&(n.textContent=e||"")}let It=null,re=null;const gd="tina4.editor.session.id";function xz(i){It=i,localStorage.removeItem(gd),Qd(),yd()}function Qd(){if(!It){NQ("No active session","");return}const i=(re==null?void 0:re.files.length)??0,e=(re==null?void 0:re.commits.length)??0,t=(re==null?void 0:re.warnings.length)??0,n=It.title||`session ${It.id.slice(0,8)}`,r=[];e&&r.push(`${e} commit${e===1?"":"s"}`),i&&r.push(`${i} file${i===1?"":"s"}`),t&&r.push(`⚠ ${t}`),NQ(n,r.join(" · "))}function yd(){const i=document.getElementById("btn-session-revise"),e=document.getElementById("btn-session-apply"),t=document.getElementById("btn-session-cancel"),n=!!It,r=((re==null?void 0:re.files.length)??0)>0;i&&(i.disabled=!n),e&&(e.disabled=!r),t&&(t.disabled=!n)}function kz(){const i=document.getElementById("diff-empty-state"),e=document.getElementById("diff-content-area");if(!i||!e)return;if(!It||!re||re.files.length===0){i.style.display="",e.setAttribute("hidden","");return}i.style.display="none",e.removeAttribute("hidden");const t=document.getElementById("diff-summary");if(t){const o=re.files.length,a=re.commits.length;t.innerHTML=`
|
|
1037
1041
|
<strong>${o}</strong> file${o===1?"":"s"} across
|
|
1038
1042
|
<strong>${a}</strong> commit${a===1?"":"s"} on
|
|
1039
1043
|
<span class="diff-sha">${Q(re.branch)}</span>
|
|
@@ -1059,11 +1063,11 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
1059
1063
|
<span class="diff-commit-subject">${Q(a.subject)}</span>
|
|
1060
1064
|
${l?`<span class="diff-commit-agent">${Q(l)}</span>`:""}
|
|
1061
1065
|
</div>
|
|
1062
|
-
`}).join("");s.innerHTML=`<div class="diff-commits-header">commits</div>${o}`}}async function
|
|
1063
|
-
`,1)[0]||"",t=e.match(/^\s*(?:\/\/|#|<!--)\s*([\w./\-]+\.\w+)\s*(?:-->)?\s*$/);return t&&t[1].includes("/")?{path:t[1],rest:i.slice(e.length+1)}:{path:null,rest:i}}function
|
|
1066
|
+
`}).join("");s.innerHTML=`<div class="diff-commits-header">commits</div>${o}`}}async function Tz(){if(!It){re=null,Qd(),yd();return}try{re=await AS(It.id)}catch(i){if(re=null,String(i).includes("not found")){xz(null);return}}Qd(),yd(),Xz(),kz()}async function vz(){const i=localStorage.getItem(gd);if(i){try{const t=(await US()).find(n=>n.id===i);if(t){It=t,await Tz();return}}catch{}localStorage.removeItem(gd)}}function Xz(){const i=document.getElementById("session-summary-strip"),e=document.getElementById("summary-chip-session");if(!i||!e)return;if(!It||!re||re.files.length===0){i.setAttribute("hidden","");return}const t=re.files.length,n=re.commits.length,r=re.warnings.length,s=[`<strong>${t}</strong> file${t===1?"":"s"}`,`<strong>${n}</strong> commit${n===1?"":"s"}`];r>0&&s.push(`<strong style="color:var(--warning,#f9e2af)">⚠ ${r}</strong>`),e.innerHTML=s.join(" · "),i.removeAttribute("hidden")}let _z=0;const hs=new Map;function Rz(i){const e=i.split(`
|
|
1067
|
+
`,1)[0]||"",t=e.match(/^\s*(?:\/\/|#|<!--)\s*([\w./\-]+\.\w+)\s*(?:-->)?\s*$/);return t&&t[1].includes("/")?{path:t[1],rest:i.slice(e.length+1)}:{path:null,rest:i}}function Zz(i){const e=l=>Q(l).replace(/`([^`]+)`/g,'<code style="background:rgba(0,0,0,0.3);padding:0.1rem 0.3rem;border-radius:0.2rem">$1</code>').replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/(^|[^*])\*([^*\n]+)\*/g,"$1<em>$2</em>"),t=i.split(`
|
|
1064
1068
|
`),n=[];let r=null,s=!1;const o=()=>{s&&(n.push("</ul>"),s=!1)},a=()=>{o(),r&&(n.push(`</${r}>`),r=null)};for(const l of t){const O=l.replace(/\s+$/,"");if(!O.trim()){a();continue}const c=O.match(/^(#{1,6})\s+(.*)$/);if(c){a(),n.push(`<h${c[1].length} style="margin:0.4rem 0 0.2rem;font-size:${1.05-c[1].length*.05}rem">${e(c[2])}</h${c[1].length}>`);continue}const d=O.match(/^(?:\s{2,}|\t)[-*]\s+(.*)$/);if(d&&r){s||(n.push('<ul style="margin:0.1rem 0 0.1rem 1.2rem">'),s=!0),n.push(`<li>${e(d[1])}</li>`);continue}const h=O.match(/^\s*\d+\.\s+(.*)$/);if(h){r!=="ol"?(a(),n.push('<ol style="margin:0.2rem 0 0.2rem 1.2rem">'),r="ol"):o(),n.push(`<li>${e(h[1])}</li>`);continue}const f=O.match(/^\s*[-*]\s+(.*)$/);if(f){r!=="ul"?(a(),n.push('<ul style="margin:0.2rem 0 0.2rem 1.2rem">'),r="ul"):o(),n.push(`<li>${e(f[1])}</li>`);continue}a(),n.push(`<p style="margin:0.2rem 0">${e(O)}</p>`)}return a(),n.join("")}function fs(i){let e=i.replace(/\\n/g,`
|
|
1065
1069
|
`);return e=e.replace(/```tool_call\s*\n[\s\S]*?(?:```|$)/g,""),e=e.replace(/```tool_result[^\n]*\n[\s\S]*?```/g,""),e=e.replace(/\n{2,}/g,`
|
|
1066
|
-
`).trim(),e.replace(/\s+/g,"").length||(e=""),e=e.replace(/```(\w*)\n?([\s\S]*?)```/g,(t,n,r)=>{const{path:s,rest:o}=
|
|
1070
|
+
`).trim(),e.replace(/\s+/g,"").length||(e=""),e=e.replace(/```(\w*)\n?([\s\S]*?)```/g,(t,n,r)=>{const{path:s,rest:o}=Rz(r),a=`aiblk_${++_z}`;hs.set(a,{code:o,path:s,lang:n||""});const l=/^(markdown|md)$/i.test(n||""),O=l?Zz(o):o.split(`
|
|
1067
1071
|
`).map(f=>f.startsWith("+")?`<span class="ai-diff-add">${Q(f)}</span>`:f.startsWith("-")?`<span class="ai-diff-del">${Q(f)}</span>`:Q(f)).join(`
|
|
1068
1072
|
`),c=s?Q(s):"active file",d=!s&&!B;return s?`<div class="ai-codeblock ai-codeblock-collapsed" data-block-id="${a}" data-auto-apply="1">
|
|
1069
1073
|
<div class="ai-codeblock-bar">
|
|
@@ -1087,7 +1091,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
1087
1091
|
</span>
|
|
1088
1092
|
</div>
|
|
1089
1093
|
<pre${l?' class="ai-codeblock-md"':""}>${l?O:`<code>${O}</code>`}</pre>
|
|
1090
|
-
</div>`}),e=e.replace(/`([^`]+)`/g,'<code style="background:rgba(0,0,0,0.3);padding:0.1rem 0.3rem;border-radius:0.2rem">$1</code>'),e=e.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>"),e=e.replace(/\n/g,"<br>"),e=e.replace(/(<div class="ai-codeblock"[\s\S]*?<\/div>)(<br>)+/g,"$1"),e=e.replace(/(<br>)+(<div class="ai-codeblock")/g,"$2"),e}async function
|
|
1094
|
+
</div>`}),e=e.replace(/`([^`]+)`/g,'<code style="background:rgba(0,0,0,0.3);padding:0.1rem 0.3rem;border-radius:0.2rem">$1</code>'),e=e.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>"),e=e.replace(/\n/g,"<br>"),e=e.replace(/(<div class="ai-codeblock"[\s\S]*?<\/div>)(<br>)+/g,"$1"),e=e.replace(/(<br>)+(<div class="ai-codeblock")/g,"$2"),e}async function zz(i){const e=hs.get(i);if(e)try{await navigator.clipboard.writeText(e.code),$i(i,"Copied")}catch{$i(i,"Copy failed",!0)}}function qz(i){const e=hs.get(i);if(!e)return;const t=ee.find(n=>n.path===B);if(!(t!=null&&t.view)){$i(i,"Open a file first",!0);return}t.view.dispatch(t.view.state.replaceSelection(e.code)),t.dirty=!0,Wi(),$i(i,"Inserted")}async function Vz(i){try{await Ue(".")}catch{}if(!i)return;const e=ee.find(t=>t.path===i);if(e)try{const t=await L(`/file?path=${encodeURIComponent(i)}`),n=(t==null?void 0:t.content)??"";e.view&&e.view.dispatch({changes:{from:0,to:e.view.state.doc.length,insert:n}}),e.content=n,e.dirty=!1,Wi(),pi(`Reloaded ${i} after tool call`)}catch{}}function Cz(i){const e=document.querySelector(`.ai-codeblock[data-block-id="${i}"]`);if(!e)return;const t=e.querySelector("pre");t&&(t.style.display=t.style.display==="none"?"":"none")}function bd(i,e,t){const n=document.querySelector(`.ai-codeblock[data-block-id="${i}"] [data-status]`);n&&(n.textContent=e,t&&(n.style.color=t))}async function Yz(i){const e=hs.get(i);if(!e)return;const t=e.path||B;if(!t){bd(i,"✗","var(--danger,#f38ba8)"),$i(i,"No target file",!0);return}try{await L("/file/save","POST",{path:t,content:e.code}),bd(i,"✓","var(--success,#a6e3a1)"),$i(i,"Applied");const n=ee.find(r=>r.path===t);n!=null&&n.view?(n.view.dispatch({changes:{from:0,to:n.view.state.doc.length,insert:e.code}}),n.content=e.code,n.dirty=!1,Wi(),pi(`Applied AI suggestion to ${t}`)):await ui(t),await Ue(".")}catch(n){bd(i,"✗","var(--danger,#f38ba8)"),$i(i,`Apply failed: ${(n==null?void 0:n.message)||n}`,!0)}}async function Wz(i){const e=hs.get(i);if(!e)return;const t=e.path||"src/new-file",n=prompt("Save to path (relative to project root):",t);if(n)try{await L("/file/save","POST",{path:n,content:e.code}),$i(i,`Saved → ${n}`),await Ue("."),await ui(n)}catch(r){$i(i,`Save failed: ${(r==null?void 0:r.message)||r}`,!0)}}function $i(i,e,t=!1){const n=document.querySelector(`.ai-codeblock[data-block-id="${i}"] .ai-codeblock-lang`);if(!n)return;const r=n.textContent;n.textContent=e,n.style.color=t?"var(--danger, #f38ba8)":"var(--success, #a6e3a1)",setTimeout(()=>{n.textContent=r,n.style.color=""},1500)}let wa=null;function Ez(i,e,t){pn();const n=document.createElement("div");n.className="editor-ctx-menu",n.style.cssText=`position:fixed;left:${i.clientX}px;top:${i.clientY}px;z-index:200`,t?n.innerHTML=`
|
|
1091
1095
|
<div class="ctx-item" onclick="window.__editorNewFile('${Q(e)}')">📄 New File <span class="ctx-shortcut">Ctrl+N</span></div>
|
|
1092
1096
|
<div class="ctx-item" onclick="window.__editorNewFolder('${Q(e)}')">📁 New Folder <span class="ctx-shortcut">Ctrl+Shift+N</span></div>
|
|
1093
1097
|
<div class="ctx-sep"></div>
|
|
@@ -1100,19 +1104,19 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
1100
1104
|
<div class="ctx-item" onclick="window.__editorDuplicate('${Q(e)}')">📋 Duplicate</div>
|
|
1101
1105
|
<div class="ctx-sep"></div>
|
|
1102
1106
|
<div class="ctx-item ctx-danger" onclick="window.__editorDelete('${Q(e)}',false)">🗑️ Delete <span class="ctx-shortcut">Del</span></div>
|
|
1103
|
-
`,document.body.appendChild(n),wa=n,setTimeout(()=>{document.addEventListener("click",pn,{once:!0})},0)}function pn(){wa&&(wa.remove(),wa=null)}async function FQ(i){pn();const e=prompt("New file name:");if(!e)return;const t=i==="."?e:`${i}/${e}`;try{await L("/file/save","POST",{path:t,content:""}),await Ue(i),ui(t)}catch(n){alert("Failed: "+n.message)}}async function HQ(i){pn();const e=prompt("New folder name:");if(!e)return;const t=i==="."?e:`${i}/${e}`;try{await L("/file/save","POST",{path:`${t}/.gitkeep`,content:""}),fi.add(t),await Ue(i)}catch(n){alert("Failed: "+n.message)}}async function KQ(i,e){pn();const t=i.split("/"),n=t.pop()||"",r=t.join("/")||".",s=prompt("Rename to:",n);if(!s||s===n)return;const o=r==="."?s:`${r}/${s}`;try{await L("/file/rename","POST",{from:i,to:o});const a=ee.findIndex(l=>l.path===i);a>=0&&(ee[a].path=o,B===i&&(B=o),Wi()),await Ue(r)}catch(a){alert("Rename failed: "+a.message)}}async function JQ(i,e){if(pn(),!!confirm(`Delete ${e?"folder":"file"} "${i}"?`))try{await L("/file/delete","POST",{path:i,is_dir:e}),ee.findIndex(s=>s.path===i)>=0&&fn(i);const r=i.split("/").slice(0,-1).join("/")||".";await Ue(r)}catch(n){alert("Delete failed: "+n.message)}}async function
|
|
1107
|
+
`,document.body.appendChild(n),wa=n,setTimeout(()=>{document.addEventListener("click",pn,{once:!0})},0)}function pn(){wa&&(wa.remove(),wa=null)}async function FQ(i){pn();const e=prompt("New file name:");if(!e)return;const t=i==="."?e:`${i}/${e}`;try{await L("/file/save","POST",{path:t,content:""}),await Ue(i),ui(t)}catch(n){alert("Failed: "+n.message)}}async function HQ(i){pn();const e=prompt("New folder name:");if(!e)return;const t=i==="."?e:`${i}/${e}`;try{await L("/file/save","POST",{path:`${t}/.gitkeep`,content:""}),fi.add(t),await Ue(i)}catch(n){alert("Failed: "+n.message)}}async function KQ(i,e){pn();const t=i.split("/"),n=t.pop()||"",r=t.join("/")||".",s=prompt("Rename to:",n);if(!s||s===n)return;const o=r==="."?s:`${r}/${s}`;try{await L("/file/rename","POST",{from:i,to:o});const a=ee.findIndex(l=>l.path===i);a>=0&&(ee[a].path=o,B===i&&(B=o),Wi()),await Ue(r)}catch(a){alert("Rename failed: "+a.message)}}async function JQ(i,e){if(pn(),!!confirm(`Delete ${e?"folder":"file"} "${i}"?`))try{await L("/file/delete","POST",{path:i,is_dir:e}),ee.findIndex(s=>s.path===i)>=0&&fn(i);const r=i.split("/").slice(0,-1).join("/")||".";await Ue(r)}catch(n){alert("Delete failed: "+n.message)}}async function Az(i){pn();const e=i.split("/"),t=e.pop()||"",n=t.includes(".")?"."+t.split(".").pop():"",s=`${n?t.slice(0,-n.length):t}-copy${n}`,o=e.join("/")||".",a=o==="."?s:`${o}/${s}`;try{const l=await L(`/file?path=${encodeURIComponent(i)}`);await L("/file/save","POST",{path:a,content:l.content}),await Ue(o),ui(a)}catch(l){alert("Duplicate failed: "+l.message)}}function Uz(){const i=document.getElementById("editor-menu-dropdown");if(!i)return;const e=i.style.display==="none";i.style.display=e?"block":"none",e&&setTimeout(()=>{const t=n=>{var r;(r=n.target)!=null&&r.closest(".editor-menu-wrapper")||(i.style.display="none"),document.removeEventListener("click",t)};document.addEventListener("click",t)},0)}async function ey(){var s;const i=document.getElementById("deps-search-input"),e=(s=i==null?void 0:i.value)==null?void 0:s.trim();if(!e)return;const t=document.getElementById("deps-search-results");t&&(t.innerHTML='<div class="text-sm text-muted" style="padding:8px;text-align:center">Searching...</div>');const n=(B==null?void 0:B.split("/").pop())||"",r=ad[n];if(r)try{const o=new AbortController,a=setTimeout(()=>o.abort(),1e4),l=await fetch(`/__dev/api/deps/search?q=${encodeURIComponent(e)}®istry=${r.registry}`,{signal:o.signal});clearTimeout(a);const c=(await l.json()).packages||[];if(!t)return;if(c.length===0){t.innerHTML='<div class="text-sm text-muted" style="padding:8px;text-align:center">No packages found</div>';return}t.innerHTML=c.map(d=>`<div class="deps-item">
|
|
1104
1108
|
<div class="deps-item-name">${Q(d.name)}</div>
|
|
1105
1109
|
<div class="deps-item-desc">${Q(d.description||"")}</div>
|
|
1106
1110
|
<div class="deps-item-meta">
|
|
1107
1111
|
<span>${Q(d.version||"")}</span>
|
|
1108
1112
|
<button class="btn btn-sm" style="font-size:0.6rem;padding:2px 8px;color:var(--success);border-color:var(--success)" onclick="window.__depsInstall('${Q(d.name)}','${Q(d.version||"")}')">+ Install</button>
|
|
1109
1113
|
</div>
|
|
1110
|
-
</div>`).join("")}catch(o){t&&(t.innerHTML=`<div class="text-sm" style="padding:8px;color:var(--danger)">${Q(o.message||"Search failed")}</div>`)}}async function
|
|
1114
|
+
</div>`).join("")}catch(o){t&&(t.innerHTML=`<div class="text-sm" style="padding:8px;color:var(--danger)">${Q(o.message||"Search failed")}</div>`)}}async function jz(i,e){const t=(B==null?void 0:B.split("/").pop())||"",n=ad[t];if(!n)return;const r=document.getElementById("deps-search-results");r&&(r.innerHTML=`<div class="text-sm text-muted" style="padding:8px;text-align:center">Installing ${Q(i)}...</div>`);const s=document.getElementById("deps-dev-toggle"),o=!!(s!=null&&s.checked);try{const a=await L("/deps/install","POST",{name:i,version:e,registry:n.registry,file:B,dev:o});if(r&&(r.innerHTML=`<div class="text-sm" style="padding:8px;color:var(--success)">✔ ${Q(a.message||`Installed ${i}`)}</div>`),B){const l=await L(`/file?path=${encodeURIComponent(B)}`),O=ee.find(c=>c.path===B);O&&l.content&&(O.content=l.content,O.dirty=!1,ld(),RQ(n))}}catch(a){r&&(r.innerHTML=`<div class="text-sm" style="padding:8px;color:var(--danger)">✗ ${Q(a.message||"Install failed")}</div>`)}}window.__depsSearch=ey,window.__depsInstall=jz;async function Mz(i){const e=prompt(`Name for the new ${i}:`);if(!e)return;const t=document.getElementById("scaffold-output");t&&(t.style.display="block",t.textContent=`Generating ${i} "${e}"...`);try{const n=await L("/scaffold/run","POST",{kind:i,name:e}),r=n.path||Dz(n.output);t&&(t.innerHTML=`<span style="color:var(--success)">✔</span> ${Q(`Created ${i}: ${e}`)}`,n.output&&(t.innerHTML+=`
|
|
1111
1115
|
<span style="opacity:0.7">${Q(String(n.output).trim())}</span>`),r&&(t.innerHTML+=`
|
|
1112
|
-
<span style="color:var(--info);cursor:pointer;text-decoration:underline" onclick="window.__editorOpenFile('${Q(r)}')">${Q(r)}</span>`)),Ue("."),r&&setTimeout(()=>ui(r),500)}catch(n){t&&(t.innerHTML=`<span style="color:var(--danger)">✗</span> ${Q(n.message||"Failed")}`)}}const
|
|
1113
|
-
${String(e.output).trim().slice(-600)}`:""}`:e.output||e.message||`${i} complete`}function
|
|
1116
|
+
<span style="color:var(--info);cursor:pointer;text-decoration:underline" onclick="window.__editorOpenFile('${Q(r)}')">${Q(r)}</span>`)),Ue("."),r&&setTimeout(()=>ui(r),500)}catch(n){t&&(t.innerHTML=`<span style="color:var(--danger)">✗</span> ${Q(n.message||"Failed")}`)}}const Lz={migrate:"/migrate",test:"/test",seed:"/seed/run"};async function Iz(i){const e=document.getElementById("scaffold-output"),t=Lz[i];if(!t){e&&(e.style.display="block",e.innerHTML=`<span style="color:var(--danger)">✗</span> Unknown command: ${Q(i)}`);return}e&&(e.style.display="block",e.textContent=`Running ${i}...`);try{const n=await L(t,"POST",{});e&&(e.innerHTML=`<span style="color:var(--success)">✔</span> ${Q(Gz(i,n))}`),(i==="migrate"||i==="seed")&&Ue(".")}catch(n){e&&(e.innerHTML=`<span style="color:var(--danger)">✗</span> ${Q(n.message||"Failed")}`)}}function Gz(i,e){var t,n,r;if(!e||typeof e!="object")return`${i} complete`;if(i==="migrate"){const s=((t=e.applied)==null?void 0:t.length)??0,o=((n=e.skipped)==null?void 0:n.length)??0,a=((r=e.failed)==null?void 0:r.length)??0;return`Migrate: ${s} applied, ${o} skipped${a?`, ${a} failed`:""}`}if(i==="seed"){const s=e.seeded??0,o=e.failed??0;return`Seed: ${s} rows${o?`, ${o} failed`:""}`}return i==="test"?`Tests ${e.ok!==!1&&e.code===0?"passed":"failed"}${e.output?`
|
|
1117
|
+
${String(e.output).trim().slice(-600)}`:""}`:e.output||e.message||`${i} complete`}function Dz(i){if(!i)return;const e=String(i).match(/((?:src|migrations|routes|models)\/[\w./-]+)/);return e?e[1]:void 0}window.__scaffold=Mz,window.__scaffoldRun=Iz;const Sd="tina4.editor.sidebar-width",wd="tina4.editor.right-panel-width";function Bz(){const i=localStorage.getItem(Sd),e=localStorage.getItem(wd),t=document.getElementById("editor-sidebar"),n=document.getElementById("editor-right-panel");t&&i&&(t.style.width=i+"px"),n&&e&&(n.style.width=e+"px")}function Nz(){const i=document.querySelector(".editor-layout"),e=document.getElementById("editor-sidebar"),t=document.getElementById("editor-right-panel"),n=document.getElementById("editor-splitter-left"),r=document.getElementById("editor-splitter-right");!i||!e||!t||!n||!r||(Bz(),ty(n,s=>{const o=parseFloat(getComputedStyle(e).width);return a=>{const l=Math.max(160,Math.min(600,o+a));return e.style.width=l+"px",l}},s=>localStorage.setItem(Sd,String(Math.round(s)))),ty(r,()=>{const s=parseFloat(getComputedStyle(t).width);return o=>{const a=Math.max(200,Math.min(800,s-o));return t.style.width=a+"px",a}},s=>localStorage.setItem(wd,String(Math.round(s)))))}function ty(i,e,t){i.addEventListener("mousedown",n=>{n.preventDefault(),i.classList.add("dragging"),document.body.style.userSelect="none",document.body.style.cursor="col-resize";const r=n.clientX,s=e(r);let o=0;const a=O=>{o=s(O.clientX-r)},l=()=>{document.removeEventListener("mousemove",a),document.removeEventListener("mouseup",l),i.classList.remove("dragging"),document.body.style.userSelect="",document.body.style.cursor="",o&&t(o)};document.addEventListener("mousemove",a),document.addEventListener("mouseup",l)}),i.addEventListener("dblclick",()=>{const n=document.getElementById("editor-sidebar"),r=document.getElementById("editor-right-panel");i.id==="editor-splitter-left"&&n&&(n.style.width="",localStorage.removeItem(Sd)),i.id==="editor-splitter-right"&&r&&(r.style.width="",localStorage.removeItem(wd))})}function Fz(i,e){document.querySelectorAll(".tab-ctx-menu").forEach(h=>h.remove());const t=ee.findIndex(h=>h.path===e),n=t>0,r=t>=0&&t<ee.length-1,s=ee.length>1,o=(h,f,p)=>`<div class="tab-ctx-item ${p?"":"disabled"}"${p?` data-action="${f}"`:""}>
|
|
1114
1118
|
<span>${h}</span>
|
|
1115
|
-
</div>`,a=document.createElement("div");a.className="tab-ctx-menu",a.innerHTML=o("Close","close",!0)+o("Close Others","close-others",s)+'<div class="tab-ctx-sep"></div>'+o("Close to the Left","close-left",n)+o("Close to the Right","close-right",r)+'<div class="tab-ctx-sep"></div>'+o("Close All","close-all",ee.length>0),a.addEventListener("click",h=>{const f=h.target.closest("[data-action]");if(!f)return;switch(f.dataset.action){case"close":fn(e);break;case"close-others":
|
|
1119
|
+
</div>`,a=document.createElement("div");a.className="tab-ctx-menu",a.innerHTML=o("Close","close",!0)+o("Close Others","close-others",s)+'<div class="tab-ctx-sep"></div>'+o("Close to the Left","close-left",n)+o("Close to the Right","close-right",r)+'<div class="tab-ctx-sep"></div>'+o("Close All","close-all",ee.length>0),a.addEventListener("click",h=>{const f=h.target.closest("[data-action]");if(!f)return;switch(f.dataset.action){case"close":fn(e);break;case"close-others":Hz(e);break;case"close-left":Kz(e);break;case"close-right":Jz(e);break;case"close-all":e2();break}a.remove()}),document.body.appendChild(a);const l=Math.min(i.clientX,window.innerWidth-190),O=Math.min(i.clientY,window.innerHeight-a.offsetHeight-10);a.style.left=l+"px",a.style.top=O+"px";const c=h=>{h&&a.contains(h.target)||(a.remove(),document.removeEventListener("mousedown",c,!0),document.removeEventListener("keydown",d,!0),document.removeEventListener("scroll",c,!0))},d=h=>{h.key==="Escape"&&c()};setTimeout(()=>{document.addEventListener("mousedown",c,!0),document.addEventListener("keydown",d,!0),document.addEventListener("scroll",c,!0)},0)}function Hz(i){ee.filter(t=>t.path!==i).map(t=>t.path).forEach(t=>fn(t))}function Kz(i){const e=ee.findIndex(n=>n.path===i);if(e<1)return;ee.slice(0,e).map(n=>n.path).forEach(n=>fn(n))}function Jz(i){const e=ee.findIndex(n=>n.path===i);if(e<0||e>=ee.length-1)return;ee.slice(e+1).map(n=>n.path).forEach(n=>fn(n))}function e2(){ee.map(e=>e.path).forEach(e=>fn(e))}window.__editorToggleMenu=Uz,window.__editorCtxMenu=Ez,window.__editorNewFile=FQ,window.__editorNewFolder=HQ,window.__editorRename=KQ,window.__editorDelete=JQ,window.__editorDuplicate=Az,window.__editorToggleDir=CZ,window.__editorOpenFile=ui,window.__editorSwitchFile=ls,window.__editorCloseFile=fn,window.__editorTabCtxMenu=Fz,window.__editorPopOut=EZ,window.__editorToggleAI=jZ,window.__editorPlanSwitch=NZ,window.__editorPlanOpen=FZ,window.__editorPlanCreate=HZ,window.__editorPlanRun=LZ,window.__editorPlanStop=IZ,window.__editorThoughtDismiss=BZ,window.__aiBlockCopy=zz,window.__aiBlockInsert=qz,window.__aiBlockApply=Yz,window.__aiBlockSaveAs=Wz,window.__aiBlockToggle=Cz,document.addEventListener("keydown",i=>{(i.ctrlKey||i.metaKey)&&i.key==="s"&&B&&(i.preventDefault(),qQ())}),document.addEventListener("keydown",i=>{const e=i.target;(e==null?void 0:e.id)==="deps-search-input"&&i.key==="Enter"&&(i.preventDefault(),ey())}),document.addEventListener("keydown",i=>{var t,n,r;if(!(!B&&!document.querySelector(".editor-layout")||((t=i.target)==null?void 0:t.tagName)==="INPUT"||((n=i.target)==null?void 0:n.tagName)==="TEXTAREA")){if((i.ctrlKey||i.metaKey)&&i.key==="n"&&!i.shiftKey){i.preventDefault();const s=B&&B.split("/").slice(0,-1).join("/")||".";FQ(s)}if((i.ctrlKey||i.metaKey)&&i.key==="N"&&i.shiftKey){i.preventDefault();const s=B&&B.split("/").slice(0,-1).join("/")||".";HQ(s)}i.key==="F2"&&B&&(i.preventDefault(),KQ(B)),i.key==="Delete"&&B&&!((r=i.target)!=null&&r.closest(".cm-editor"))&&(i.preventDefault(),JQ(B,!1))}}),(function(){const e=()=>{document.querySelectorAll(".tina4-fb-btn, .tina4-fb-modal").forEach(t=>t.remove()),window.__tina4FeedbackLoaded=!0};e(),requestAnimationFrame(e),setTimeout(e,500),setTimeout(e,2e3)})();const iy=document.createElement("style");iy.textContent=ly,document.head.appendChild(iy);const Pa=oy();ay(Pa);let ny=[{id:"editor",label:"Code With Me",render:RZ},...[{id:"routes",label:"Routes",render:Oy},{id:"database",label:"Database",render:cy},{id:"graphql",label:"GraphQL",render:Ry},{id:"queue",label:"Queue",render:Vy},{id:"errors",label:"Errors",render:yy},{id:"metrics",label:"Metrics",render:Ty},{id:"system",label:"System",render:Py}]],Pd="editor";function t2(){const i=document.getElementById("app");if(!i)return;i.innerHTML=`
|
|
1116
1120
|
<div class="dev-admin">
|
|
1117
1121
|
<div class="dev-header">
|
|
1118
1122
|
<h1><span>Tina4</span> Dev Admin</h1>
|
|
@@ -1124,4 +1128,4 @@ ${String(e.output).trim().slice(-600)}`:""}`:e.output||e.message||`${i} complete
|
|
|
1124
1128
|
<div class="dev-tabs" id="tab-bar"></div>
|
|
1125
1129
|
<div class="dev-content" id="tab-content"></div>
|
|
1126
1130
|
</div>
|
|
1127
|
-
`;const e=document.getElementById("tab-bar");e.innerHTML=ny.map(t=>`<button class="dev-tab ${t.id===Pd?"active":""}" data-tab="${t.id}" onclick="window.__switchTab('${t.id}')">${t.label}</button>`).join(""),ry(Pd)}function ry(i){Pd=i;const e=document.querySelector(".dev-header"),t=document.getElementById("tab-bar"),n=document.querySelector(".dev-admin"),r=i==="editor";e&&(e.style.display=r?"none":""),t&&(t.style.display=r?"none":""),n&&n.classList.toggle("fullscreen-editor",r),document.querySelectorAll(".dev-tab").forEach(l=>{l.classList.toggle("active",l.dataset.tab===i)});const s=document.getElementById("tab-content");if(!s)return;const o=document.createElement("div");o.className="dev-panel active",s.innerHTML="",s.appendChild(o);const a=ny.find(l=>l.id===i);a&&a.render(o)}function
|
|
1131
|
+
`;const e=document.getElementById("tab-bar");e.innerHTML=ny.map(t=>`<button class="dev-tab ${t.id===Pd?"active":""}" data-tab="${t.id}" onclick="window.__switchTab('${t.id}')">${t.label}</button>`).join(""),ry(Pd)}function ry(i){Pd=i;const e=document.querySelector(".dev-header"),t=document.getElementById("tab-bar"),n=document.querySelector(".dev-admin"),r=i==="editor";e&&(e.style.display=r?"none":""),t&&(t.style.display=r?"none":""),n&&n.classList.toggle("fullscreen-editor",r),document.querySelectorAll(".dev-tab").forEach(l=>{l.classList.toggle("active",l.dataset.tab===i)});const s=document.getElementById("tab-content");if(!s)return;const o=document.createElement("div");o.className="dev-panel active",s.innerHTML="",s.appendChild(o);const a=ny.find(l=>l.id===i);a&&a.render(o)}function i2(){if(window.parent!==window)try{const i=window.parent.document.getElementById("tina4-dev-panel");i&&i.remove()}catch{document.body.style.display="none"}}window.__closeDevAdmin=i2,window.__switchTab=ry,t2(),L("/system").then(i=>{const e=document.getElementById("version-label"),t=i.version||(typeof i.framework=="object"?i.framework.version:null)||(typeof i.framework=="string"?i.framework:null);e&&t&&(e.innerHTML=`${Pa.name} • v${Q(t)}`)}).catch(()=>{const i=document.getElementById("version-label");i&&(i.innerHTML=`${Pa.name}`)})})();
|