pinokiod 6.0.70 → 6.0.72

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.
@@ -364,8 +364,6 @@ class Api {
364
364
  delete this.running[requestPath]
365
365
  delete this.kernel.memory.local[requestPath]
366
366
  }
367
-
368
-
369
367
  this.ondata({
370
368
  id: req.params.id || requestPath,
371
369
  type: "disconnect"
@@ -924,6 +922,10 @@ class Api {
924
922
  git: this.parentGitURI(request.path),
925
923
  cwd,
926
924
  origin: request.origin,
925
+ caller: request.caller,
926
+ action: request.action,
927
+ args,
928
+ client: request.client,
927
929
  body: script
928
930
  }
929
931
  // 7. resolve the rpc
@@ -1202,7 +1204,7 @@ class Api {
1202
1204
  index: i,
1203
1205
  total,
1204
1206
  type: "result",
1205
- data: result,
1207
+ data: result
1206
1208
  })
1207
1209
 
1208
1210
 
@@ -1246,7 +1248,6 @@ class Api {
1246
1248
  return
1247
1249
  }
1248
1250
 
1249
-
1250
1251
  if (typeof rpc.next === "undefined" || rpc.next === null) {
1251
1252
 
1252
1253
 
@@ -1,9 +1,146 @@
1
- const path = require('path')
2
1
  class Script {
2
+ currentParent(req) {
3
+ if (req && req.parent && typeof req.parent === "object") {
4
+ return req.parent
5
+ }
6
+ return null
7
+ }
8
+ currentPath(req) {
9
+ const parent = this.currentParent(req)
10
+ if (parent && parent.path) {
11
+ return parent.path
12
+ }
13
+ return null
14
+ }
15
+ currentSessionId(req) {
16
+ if (req && typeof req.id === "string" && req.id.trim()) {
17
+ return req.id
18
+ }
19
+ const parent = this.currentParent(req)
20
+ if (parent && typeof parent.id === "string" && parent.id.trim()) {
21
+ return parent.id
22
+ }
23
+ return undefined
24
+ }
25
+ currentCaller(req) {
26
+ if (req && typeof req.caller === "string" && req.caller.trim()) {
27
+ return req.caller
28
+ }
29
+ const parent = this.currentParent(req)
30
+ if (parent && typeof parent.caller === "string" && parent.caller.trim()) {
31
+ return parent.caller
32
+ }
33
+ return undefined
34
+ }
35
+ currentClient(req) {
36
+ if (req && req.client) {
37
+ return req.client
38
+ }
39
+ const parent = this.currentParent(req)
40
+ if (parent && parent.client) {
41
+ return parent.client
42
+ }
43
+ return undefined
44
+ }
45
+ currentOrigin(req) {
46
+ if (req && typeof req.origin === "string" && req.origin.trim()) {
47
+ return req.origin
48
+ }
49
+ const parent = this.currentParent(req)
50
+ if (parent && typeof parent.origin === "string" && parent.origin.trim()) {
51
+ return parent.origin
52
+ }
53
+ return undefined
54
+ }
55
+ buildStartRequest(req, uri, input, target) {
56
+ const nextParams = Object.assign({}, req && req.params ? req.params : {}, {
57
+ uri,
58
+ params: input
59
+ })
60
+ const preserveSession = !!(target && target.self)
61
+ return {
62
+ id: preserveSession ? this.currentSessionId(req) : undefined,
63
+ caller: preserveSession ? this.currentCaller(req) : undefined,
64
+ cwd: req ? req.cwd : undefined,
65
+ client: this.currentClient(req),
66
+ origin: this.currentOrigin(req),
67
+ params: nextParams
68
+ }
69
+ }
70
+ resolveRestartTarget(req, kernel) {
71
+ const hasParams = req && req.params && typeof req.params === "object"
72
+ const requestedUri = hasParams && typeof req.params.uri === "string" && req.params.uri.trim()
73
+ ? req.params.uri
74
+ : null
75
+ const currentPath = this.currentPath(req)
76
+ if (!requestedUri) {
77
+ if (!currentPath) {
78
+ throw new Error("script.restart requires params.uri when called outside a running script")
79
+ }
80
+ return {
81
+ displayUri: currentPath,
82
+ startUri: currentPath,
83
+ stopUri: currentPath,
84
+ self: true,
85
+ }
86
+ }
87
+ const stopUri = kernel.api.filePath(requestedUri, req.cwd)
88
+ return {
89
+ displayUri: requestedUri,
90
+ startUri: requestedUri,
91
+ stopUri,
92
+ self: currentPath ? stopUri === currentPath : false,
93
+ }
94
+ }
95
+ resolveRestartInput(req, kernel, target) {
96
+ const hasExplicitParams = !!(req && req.params && typeof req.params === "object" && Object.prototype.hasOwnProperty.call(req.params, "params"))
97
+ if (hasExplicitParams) {
98
+ return req.params.params
99
+ }
100
+ if (target && target.self && req && req.parent && Object.prototype.hasOwnProperty.call(req.parent, "args")) {
101
+ return req.parent.args
102
+ }
103
+ if (kernel && kernel.memory && kernel.memory.args && target) {
104
+ return kernel.memory.args[target.stopUri]
105
+ }
106
+ return undefined
107
+ }
108
+ scheduleStart(req, ondata, kernel) {
109
+ setTimeout(() => {
110
+ this.start(req, ondata, kernel).catch((e) => {
111
+ const stack = e && e.stack ? e.stack : String(e)
112
+ ondata({ raw: `\r\nFailed to start ${req.params.uri}\r\n${stack}\r\n` })
113
+ })
114
+ }, 0)
115
+ }
3
116
  async start(req, ondata, kernel) {
4
117
  let res = await this.run(req, ondata, kernel)
5
118
  return res
6
119
  }
120
+ async restart(req, ondata, kernel) {
121
+ if (!req.params) {
122
+ req.params = {}
123
+ }
124
+ const target = this.resolveRestartTarget(req, kernel)
125
+ const input = this.resolveRestartInput(req, kernel, target)
126
+ const sessionId = target.self ? this.currentSessionId(req) : undefined
127
+ ondata({
128
+ id: sessionId || target.stopUri,
129
+ }, "restart")
130
+ const stopRequest = sessionId
131
+ ? { params: { id: sessionId } }
132
+ : { params: { uri: target.stopUri } }
133
+ await kernel.api.stop(stopRequest)
134
+ ondata({ raw: `\r\nRestarting ${target.displayUri}\r\n` })
135
+ const startRequest = this.buildStartRequest(req, target.startUri, input, target)
136
+ this.scheduleStart(startRequest, ondata, kernel)
137
+ return {
138
+ uri: target.displayUri,
139
+ scheduled: true,
140
+ self: target.self,
141
+ params: input,
142
+ }
143
+ }
7
144
  async stop(req, ondata, kernel) {
8
145
  /*
9
146
  {
@@ -20,7 +157,7 @@ class Script {
20
157
  }
21
158
  for(let uri of uris) {
22
159
  kernel.api.stop({ params: { uri } })
23
- ondata({ raw: `\r\nStopped ${req.params.uri}\r\n` })
160
+ ondata({ raw: `\r\nStopped ${uri}\r\n` })
24
161
  }
25
162
  }
26
163
  async run(req, ondata, kernel) {
@@ -50,12 +187,25 @@ class Script {
50
187
  // if not already running, start.
51
188
  let uri = await this.download(req, ondata, kernel)
52
189
  let res = await new Promise((resolve, reject) => {
53
- kernel.api.process({
190
+ let request = {
54
191
  uri,
55
192
  input: req.params.params,
56
- client: req.client,
57
- caller: req.parent.path,
58
- }, (r) => {
193
+ client: this.currentClient(req),
194
+ }
195
+ if (req.id) {
196
+ request.id = req.id
197
+ }
198
+ const caller = this.currentCaller(req)
199
+ if (caller) {
200
+ request.caller = caller
201
+ } else if (req.parent && req.parent.path) {
202
+ request.caller = req.parent.path
203
+ }
204
+ const origin = this.currentOrigin(req)
205
+ if (origin) {
206
+ request.origin = origin
207
+ }
208
+ kernel.api.process(request, (r) => {
59
209
  resolve(r.input)
60
210
  })
61
211
  })
@@ -125,7 +275,7 @@ class Script {
125
275
 
126
276
  await kernel.api.init()
127
277
  } else {
128
- uri = path.resolve(req.cwd, req.params.uri)
278
+ uri = kernel.api.filePath(req.params.uri, req.cwd)
129
279
  }
130
280
  return uri
131
281
  }
package/kernel/index.js CHANGED
@@ -250,10 +250,29 @@ class Kernel {
250
250
  }
251
251
  return o
252
252
  }
253
+ scopedMemoryEntry(store, filePath) {
254
+ if (!store || !filePath) {
255
+ return null
256
+ }
257
+ const exact = store[filePath]
258
+ if (exact) {
259
+ return exact
260
+ }
261
+ const wantsExactMatch = this.stripSessionParam(filePath) !== filePath
262
+ if (wantsExactMatch) {
263
+ return null
264
+ }
265
+ for (const [key, value] of Object.entries(store)) {
266
+ if (value && this.stripSessionParam(key) === filePath) {
267
+ return value
268
+ }
269
+ }
270
+ return null
271
+ }
253
272
  local(...args) {
254
273
  // get local variables at path
255
274
  let filePath = this.api.filePath(path.resolve(...args))
256
- let v = this.memory.local[filePath]
275
+ let v = this.scopedMemoryEntry(this.memory.local, filePath)
257
276
  //let v = this.memory.local[path.resolve(...args)]
258
277
  if (v) {
259
278
  return v
@@ -264,7 +283,7 @@ class Kernel {
264
283
  global(...args) {
265
284
  // get local variables at path
266
285
  let filePath = this.api.filePath(path.resolve(...args))
267
- let v = this.memory.global[filePath]
286
+ let v = this.scopedMemoryEntry(this.memory.global, filePath)
268
287
  // let v = this.memory.global[path.resolve(...args)]
269
288
  if (v) {
270
289
  return v
@@ -275,9 +294,27 @@ class Kernel {
275
294
  running(...args) {
276
295
  return this.status(path.resolve(...args))
277
296
  }
297
+ stripSessionParam(value) {
298
+ if (typeof value !== "string" || value.length === 0) {
299
+ return value
300
+ }
301
+ return value.replace(/([?&])session=[^&]+/g, "").replace(/[?&]$/, "")
302
+ }
278
303
  status(uri, cwd) {
279
304
  let id = this.api.filePath(uri, cwd)
280
- return this.api.running[id]
305
+ if (this.api.running[id]) {
306
+ return this.api.running[id]
307
+ }
308
+ const wantsExactMatch = this.stripSessionParam(id) !== id
309
+ if (wantsExactMatch) {
310
+ return false
311
+ }
312
+ for (const [key, value] of Object.entries(this.api.running || {})) {
313
+ if (value && this.stripSessionParam(key) === id) {
314
+ return value
315
+ }
316
+ }
317
+ return false
281
318
  }
282
319
  url (origin, _path, _type) {
283
320
  /*
package/kernel/info.js CHANGED
@@ -49,8 +49,7 @@ class Info {
49
49
  running(...args) {
50
50
  let cwd = path.dirname(this.caller())
51
51
  let resolved_path = path.resolve(cwd, ...args)
52
- let running = this.kernel.api.running[resolved_path]
53
- return running ? running : false
52
+ return this.kernel.status(resolved_path)
54
53
  }
55
54
  exists(...args) {
56
55
  let cwd = path.dirname(this.caller())
@@ -61,12 +60,12 @@ class Info {
61
60
  local(...args) {
62
61
  let cwd = path.dirname(this.caller())
63
62
  let resolved_path = path.resolve(cwd, ...args)
64
- return this.kernel.memory.local[resolved_path] || {}
63
+ return this.kernel.scopedMemoryEntry(this.kernel.memory.local, resolved_path) || {}
65
64
  }
66
65
  global(...args) {
67
66
  let cwd = path.dirname(this.caller())
68
67
  let resolved_path = path.resolve(cwd, ...args)
69
- return this.kernel.memory.global[resolved_path] || {}
68
+ return this.kernel.scopedMemoryEntry(this.kernel.memory.global, resolved_path) || {}
70
69
  }
71
70
  scriptsByApi() {
72
71
  if (!this.kernel || !this.kernel.memory || !this.kernel.memory.local) {
package/kernel/shells.js CHANGED
@@ -182,6 +182,60 @@ class Shells {
182
182
  }
183
183
  }
184
184
  const parentMeta = params.$parent || null
185
+ const queueTriggerAction = async (action, eventMatch) => {
186
+ if (!action) {
187
+ return
188
+ }
189
+ if (!parentMeta || !parentMeta.path) {
190
+ throw new Error(`unable to trigger "${action}" without a parent script`)
191
+ }
192
+
193
+ const runningKey = parentMeta.id || parentMeta.path
194
+ if (runningKey && !this.kernel.api.running[runningKey]) {
195
+ return
196
+ }
197
+
198
+ let triggerScript = parentMeta.body
199
+ let triggerCwd = parentMeta.cwd
200
+ if (!triggerScript || !Array.isArray(triggerScript[action])) {
201
+ const resolved = await this.kernel.api.resolveScript(parentMeta.path)
202
+ triggerScript = resolved.script
203
+ if (!triggerCwd) {
204
+ triggerCwd = resolved.cwd
205
+ }
206
+ }
207
+
208
+ const triggerSteps = (triggerScript && Array.isArray(triggerScript[action])) ? triggerScript[action] : null
209
+ if (!triggerSteps || triggerSteps.length === 0) {
210
+ throw new Error(`missing or invalid attribute: ${action}`)
211
+ }
212
+
213
+ const request = {
214
+ id: parentMeta.id,
215
+ uri: parentMeta.uri,
216
+ path: parentMeta.path,
217
+ git: parentMeta.git,
218
+ cwd: triggerCwd || parentMeta.cwd,
219
+ origin: parentMeta.origin,
220
+ caller: parentMeta.caller,
221
+ client: parentMeta.client,
222
+ action
223
+ }
224
+ const input = {
225
+ event: eventMatch,
226
+ trigger: action
227
+ }
228
+
229
+ this.kernel.api.queue(
230
+ request,
231
+ triggerSteps[0],
232
+ input,
233
+ 0,
234
+ triggerSteps.length,
235
+ triggerCwd || parentMeta.cwd,
236
+ parentMeta.args
237
+ )
238
+ }
185
239
 
186
240
  const plannedShell = params.shell || (this.kernel.platform === 'win32' ? 'cmd.exe' : 'bash')
187
241
  await this.ensureBracketedPasteSupport(plannedShell)
@@ -193,6 +247,10 @@ class Shells {
193
247
 
194
248
  let m
195
249
  let matched_index
250
+ const onceHandlers = new Set()
251
+ const handlerLastMatchEnd = new Map()
252
+ let liveEventBuffer = ""
253
+ let liveEventOffset = 0
196
254
 
197
255
  // if error doesn't exist, add default "error:" event
198
256
  if (!params.on) {
@@ -224,6 +282,8 @@ class Shells {
224
282
  event: <regex>,
225
283
  done: true|false,
226
284
  kill: true|false,
285
+ trigger: <alternate script action>,
286
+ once: true|false,
227
287
  debug: true|false,
228
288
  notify: {
229
289
  title,
@@ -236,9 +296,20 @@ class Shells {
236
296
  }
237
297
  */
238
298
  try {
299
+ const rawChunk = typeof stream.raw === "string"
300
+ ? stream.raw.replaceAll(/[\r\n]/g, "")
301
+ : ""
302
+ if (rawChunk.length > 0) {
303
+ liveEventBuffer = (liveEventBuffer + rawChunk).slice(-300)
304
+ liveEventOffset += rawChunk.length
305
+ }
306
+ const liveEventBufferStart = Math.max(0, liveEventOffset - liveEventBuffer.length)
239
307
  if (params.on && Array.isArray(params.on)) {
240
308
  for(let i=0; i<params.on.length; i++) {
241
309
  let handler = params.on[i]
310
+ if (handler.once && onceHandlers.has(i)) {
311
+ continue
312
+ }
242
313
  // regexify
243
314
  //let matches = /^\/([^\/]+)\/([dgimsuy]*)$/.exec(handler.event)
244
315
  if (handler.event) {
@@ -251,6 +322,9 @@ class Shells {
251
322
  let re = new RegExp(matches[1], matches[2])
252
323
  let test = re.exec(sh.monitor)
253
324
  if (test && test.length > 0) {
325
+ if (handler.once) {
326
+ onceHandlers.add(i)
327
+ }
254
328
  // reset monitor
255
329
  sh.monitor = ""
256
330
  let params = this.kernel.template.render(handler.notify, { event: test })
@@ -265,20 +339,35 @@ class Shells {
265
339
  matches[2] += "g" // if g option is not included, include it (need it for matchAll)
266
340
  }
267
341
  let re = new RegExp(matches[1], matches[2])
268
- if (stream.cleaned) {
269
- let line = stream.cleaned.replaceAll(/[\r\n]/g, "")
270
- let rendered_event = [...line.matchAll(re)]
342
+ if (liveEventBuffer.length > 0) {
343
+ const lastHandledEnd = handlerLastMatchEnd.get(i) || 0
344
+ let rendered_event = [...liveEventBuffer.matchAll(re)].filter((match) => {
345
+ const absoluteEnd = liveEventBufferStart + match.index + match[0].length
346
+ return absoluteEnd > lastHandledEnd
347
+ })
271
348
  // 3. if the rendered expression is truthy, run the "run" script
272
349
  if (rendered_event.length > 0) {
350
+ if (handler.once) {
351
+ onceHandlers.add(i)
352
+ }
353
+ const lastMatch = rendered_event[rendered_event.length - 1]
354
+ handlerLastMatchEnd.set(i, liveEventBufferStart + lastMatch.index + lastMatch[0].length)
273
355
  stream.matches = rendered_event
356
+ m = rendered_event[0]
357
+ matched_index = i
358
+ if (typeof handler.trigger === "string" && handler.trigger.trim()) {
359
+ const triggerAction = handler.trigger.trim()
360
+ queueTriggerAction(triggerAction, rendered_event[0]).catch((e) => {
361
+ console.log("Trigger error", e)
362
+ if (ondata) {
363
+ ondata({ raw: (e && e.stack) ? e.stack : String(e) })
364
+ }
365
+ })
366
+ }
274
367
  if (handler.kill) {
275
- m = rendered_event[0]
276
- matched_index = i
277
368
  sh.kill()
278
369
  }
279
370
  if (handler.done) {
280
- m = rendered_event[0]
281
- matched_index = i
282
371
  sh.continue()
283
372
  }
284
373
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "6.0.70",
3
+ "version": "6.0.72",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -3714,7 +3714,7 @@ class Server {
3714
3714
  let relpath = path.relative(this.kernel.homedir, fullpath)
3715
3715
  if (relpath.startsWith("api")) {
3716
3716
  // api script
3717
- if (this.kernel.api.running[fullpath]) {
3717
+ if (this.kernel.status(fullpath)) {
3718
3718
  menuitem.running = true
3719
3719
  }
3720
3720
  } else {
@@ -3086,28 +3086,6 @@ aside .qr {
3086
3086
  .app-icon {
3087
3087
  display: block;
3088
3088
  }
3089
- header .runner {
3090
- gap: 6px;
3091
- }
3092
- header .runner .btn {
3093
- display: flex;
3094
- flex-direction: column;
3095
- align-items: center;
3096
- gap: 2px;
3097
- padding: 6px 8px;
3098
- font-size: 10px;
3099
- line-height: 1.1;
3100
- text-align: center;
3101
- }
3102
- header .runner .btn span {
3103
- display: flex;
3104
- flex-direction: column;
3105
- align-items: center;
3106
- gap: 2px;
3107
- }
3108
- header .runner .btn i {
3109
- font-size: 14px;
3110
- }
3111
3089
  aside .tab.submenu {
3112
3090
  padding: 10px;
3113
3091
  }
@@ -4325,6 +4325,11 @@ body.dark .snapshot-footer-input input {
4325
4325
  .snapshot-footer-input input::placeholder {
4326
4326
  opacity: 0.7;
4327
4327
  }
4328
+ .mobile-bottom-nav,
4329
+ .mobile-sheet-actions,
4330
+ .mobile-menu-fallback {
4331
+ display: none;
4332
+ }
4328
4333
  header.navheader h1 {
4329
4334
  position: relative;
4330
4335
  }
@@ -4348,7 +4353,20 @@ header.navheader .mode-selector .community-mode-toggle {
4348
4353
  }
4349
4354
  @media only screen and (max-width: 768px) {
4350
4355
  :root {
4351
- --mobile-bottom-bar-height: 56px;
4356
+ --mobile-bottom-bar-height: calc(33px + env(safe-area-inset-bottom));
4357
+ }
4358
+ header.navheader {
4359
+ position: fixed !important;
4360
+ left: -10000px !important;
4361
+ top: 0 !important;
4362
+ width: auto !important;
4363
+ min-width: 0 !important;
4364
+ max-width: none !important;
4365
+ opacity: 0 !important;
4366
+ pointer-events: none !important;
4367
+ }
4368
+ header.navheader .mode-selector {
4369
+ display: none !important;
4352
4370
  }
4353
4371
  body:not(.mobile-menu-open) .appcanvas > aside {
4354
4372
  display: none;
@@ -4356,82 +4374,119 @@ header.navheader .mode-selector .community-mode-toggle {
4356
4374
  body:not(.mobile-menu-open) .appcanvas-resizer {
4357
4375
  display: none;
4358
4376
  }
4359
- header.navheader .mode-selector .community-mode-toggle {
4360
- display: flex;
4361
- }
4362
- #menu-mobile {
4363
- display: flex;
4364
- }
4365
4377
  body.mobile-menu-open #menu-mobile-close {
4366
4378
  display: inline-flex;
4367
4379
  }
4368
4380
  body.mobile-menu-open #layout-toggle {
4369
4381
  display: none;
4370
4382
  }
4371
- body.mobile-menu-open header.navheader h1 > :not(.mode-selector) {
4372
- display: none !important;
4383
+ .browserview-shell {
4384
+ box-sizing: border-box;
4373
4385
  }
4374
- header.navheader .mode-selector {
4375
- position: fixed;
4376
- left: 0;
4377
- right: 0;
4378
- bottom: 0;
4379
- top: auto;
4380
- transform: none;
4381
- margin: 0;
4382
- padding: 6px 10px calc(6px + env(safe-area-inset-bottom)) 10px;
4383
- border-radius: 0;
4384
- border-top: 1px solid rgba(0,0,0,0.08);
4385
- background: rgba(255,255,255,0.95);
4386
- flex-direction: row;
4386
+ .mobile-bottom-nav {
4387
+ position: relative;
4388
+ display: flex;
4389
+ flex: 0 0 auto;
4387
4390
  align-items: center;
4388
- justify-content: space-around;
4389
- flex-wrap: nowrap;
4390
- gap: 6px;
4391
- z-index: 10000000;
4392
- box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.08);
4391
+ gap: 3px;
4392
+ width: 100%;
4393
+ min-height: 32px;
4394
+ padding: 0 4px env(safe-area-inset-bottom) 4px;
4395
+ border-top: 1px solid rgba(0,0,0,0.08);
4396
+ background: rgba(255,255,255,0.96);
4397
+ backdrop-filter: blur(20px);
4398
+ z-index: 10000002;
4399
+ box-sizing: border-box;
4393
4400
  }
4394
- body.dark header.navheader .mode-selector {
4401
+ body.dark .mobile-bottom-nav {
4395
4402
  background: rgba(15, 17, 21, 0.96);
4396
4403
  border-top-color: rgba(255,255,255,0.08);
4397
- box-shadow: 0 -6px 18px rgba(2, 6, 20, 0.45);
4398
4404
  }
4399
- header.navheader .mode-selector .btn2 {
4400
- color: #6b7280;
4401
- background: transparent;
4402
- border-radius: 0;
4403
- flex-direction: column;
4405
+ .mobile-bottom-nav__button {
4406
+ flex: 0 0 28px;
4407
+ min-width: 28px;
4408
+ max-width: 28px;
4409
+ min-height: 28px;
4410
+ display: flex;
4404
4411
  align-items: center;
4405
4412
  justify-content: center;
4406
- gap: 2px;
4407
- padding: 6px 0;
4408
- position: relative;
4409
- width: auto;
4410
- max-width: none;
4413
+ gap: 0;
4414
+ padding: 0;
4415
+ border-radius: 7px;
4416
+ background: transparent;
4417
+ color: #64748b;
4418
+ text-decoration: none;
4411
4419
  }
4412
- body.dark header.navheader .mode-selector .btn2 {
4413
- color: rgba(148, 163, 184, 0.9);
4420
+ .mobile-bottom-nav__button i {
4421
+ font-size: 14px;
4414
4422
  }
4415
- header.navheader .mode-selector .btn2.selected {
4423
+ .mobile-bottom-nav__button.selected {
4424
+ background: rgba(10, 132, 255, 0.12);
4416
4425
  color: #0a84ff;
4417
- background: transparent;
4418
- font-weight: 600;
4419
4426
  }
4420
- body.dark header.navheader .mode-selector .btn2.selected {
4421
- color: #0a84ff;
4427
+ body.dark .mobile-bottom-nav__button {
4428
+ color: rgba(148, 163, 184, 0.9);
4422
4429
  }
4423
- header.navheader .mode-selector .btn2 {
4424
- flex: 1 1 0;
4425
- min-width: 0;
4430
+ body.dark .mobile-bottom-nav__button.selected {
4431
+ background: rgba(10, 132, 255, 0.18);
4432
+ color: #63a9ff;
4426
4433
  }
4427
- header.navheader .mode-selector .btn2 .caption {
4434
+ .mobile-bottom-nav__logo {
4435
+ width: 25px;
4436
+ height: 25px;
4437
+ object-fit: contain;
4428
4438
  display: block;
4439
+ }
4440
+ .mobile-bottom-nav__modes {
4441
+ flex: 1 1 auto;
4442
+ min-width: 0;
4443
+ display: flex;
4444
+ align-items: center;
4445
+ gap: 0;
4446
+ min-height: 26px;
4447
+ padding: 0;
4448
+ border-radius: 8px;
4449
+ border: 1px solid rgba(0,0,0,0.08);
4450
+ background: rgba(15, 23, 42, 0.06);
4451
+ }
4452
+ body.dark .mobile-bottom-nav__modes {
4453
+ border-color: rgba(255,255,255,0.08);
4454
+ background: rgba(255,255,255,0.06);
4455
+ }
4456
+ .mobile-mode-segment {
4457
+ flex: 1 1 0;
4458
+ min-width: 0;
4459
+ display: flex;
4460
+ align-items: center;
4461
+ justify-content: center;
4462
+ min-height: 24px;
4463
+ padding: 0 4px;
4464
+ border-radius: 7px;
4465
+ color: #475569;
4466
+ text-decoration: none;
4429
4467
  font-size: 10px;
4468
+ font-weight: 700;
4430
4469
  line-height: 1;
4470
+ text-align: center;
4471
+ white-space: nowrap;
4431
4472
  }
4432
- .browserview-shell {
4433
- padding-bottom: var(--mobile-bottom-bar-height);
4434
- box-sizing: border-box;
4473
+ .mobile-mode-segment.selected {
4474
+ background: #111827;
4475
+ color: #fff;
4476
+ }
4477
+ body.dark .mobile-mode-segment {
4478
+ color: rgba(226, 232, 240, 0.88);
4479
+ }
4480
+ body.dark .mobile-mode-segment.selected {
4481
+ background: #fff;
4482
+ color: #0f1115;
4483
+ }
4484
+ .appcanvas.community-mode .mobile-bottom-nav .mobile-mode-segment.selected {
4485
+ background: transparent;
4486
+ color: #475569;
4487
+ }
4488
+ body.dark .appcanvas.community-mode .mobile-bottom-nav .mobile-mode-segment.selected {
4489
+ color: rgba(226, 232, 240, 0.88);
4435
4490
  }
4436
4491
  body.mobile-menu-open .appcanvas > aside {
4437
4492
  display: flex;
@@ -4484,6 +4539,76 @@ header.navheader .mode-selector .community-mode-toggle {
4484
4539
  gap: 8px;
4485
4540
  width: 100%;
4486
4541
  }
4542
+ body.mobile-menu-open .appcanvas > aside .mobile-sheet-actions {
4543
+ display: flex;
4544
+ align-items: center;
4545
+ gap: 8px;
4546
+ width: 100%;
4547
+ overflow-x: auto;
4548
+ overflow-y: hidden;
4549
+ padding-bottom: 2px;
4550
+ scrollbar-width: none;
4551
+ }
4552
+ body.mobile-menu-open .appcanvas > aside .mobile-sheet-actions::-webkit-scrollbar,
4553
+ body.mobile-menu-open .mobile-menu-fallback .mobile-sheet-actions::-webkit-scrollbar {
4554
+ display: none;
4555
+ }
4556
+ body.mobile-menu-open .appcanvas > aside .mobile-sheet-action,
4557
+ body.mobile-menu-open .mobile-menu-fallback .mobile-sheet-action {
4558
+ flex: 0 0 auto;
4559
+ width: 40px;
4560
+ height: 40px;
4561
+ min-width: 40px;
4562
+ min-height: 40px;
4563
+ padding: 0;
4564
+ border-radius: 12px;
4565
+ border: 1px solid rgba(0,0,0,0.06);
4566
+ background: #fff;
4567
+ color: #0f172a;
4568
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
4569
+ box-sizing: border-box;
4570
+ text-decoration: none;
4571
+ display: inline-flex;
4572
+ align-items: center;
4573
+ justify-content: center;
4574
+ }
4575
+ body.dark.mobile-menu-open .appcanvas > aside .mobile-sheet-action,
4576
+ body.dark.mobile-menu-open .mobile-menu-fallback .mobile-sheet-action {
4577
+ background: rgba(15, 17, 21, 0.82);
4578
+ border-color: rgba(255,255,255,0.08);
4579
+ color: rgba(226, 232, 240, 0.92);
4580
+ box-shadow: 0 1px 2px rgba(2, 6, 20, 0.4);
4581
+ }
4582
+ body.mobile-menu-open .appcanvas > aside .mobile-sheet-action i,
4583
+ body.mobile-menu-open .mobile-menu-fallback .mobile-sheet-action i {
4584
+ font-size: 15px;
4585
+ }
4586
+ body.mobile-menu-open .appcanvas > aside .mobile-sheet-action span,
4587
+ body.mobile-menu-open .mobile-menu-fallback .mobile-sheet-action span {
4588
+ display: none;
4589
+ }
4590
+ body.mobile-menu-open .appcanvas > aside .menu-scroller .mobile-sheet-action,
4591
+ body.mobile-menu-open .mobile-menu-fallback .mobile-sheet-action {
4592
+ width: 40px !important;
4593
+ height: 40px !important;
4594
+ min-width: 40px !important;
4595
+ min-height: 40px !important;
4596
+ padding: 0 !important;
4597
+ border-radius: 12px !important;
4598
+ display: inline-flex;
4599
+ align-items: center;
4600
+ justify-content: center;
4601
+ gap: 0;
4602
+ }
4603
+ body.mobile-menu-open[data-view='files'] .mobile-menu-fallback .mobile-sheet-actions {
4604
+ display: flex;
4605
+ align-items: center;
4606
+ gap: 8px;
4607
+ width: 100%;
4608
+ overflow-x: auto;
4609
+ overflow-y: hidden;
4610
+ padding-bottom: 2px;
4611
+ }
4487
4612
  body.mobile-menu-open .appcanvas > aside .menu-scroller .btn:not(.btn2),
4488
4613
  body.mobile-menu-open .appcanvas > aside .menu-actions .btn:not(.btn2) {
4489
4614
  max-width: none;
@@ -4518,20 +4643,42 @@ header.navheader .mode-selector .community-mode-toggle {
4518
4643
  font-size: 15px;
4519
4644
  }
4520
4645
  body.mobile-menu-open .appcanvas > aside .menu-actions {
4646
+ display: none;
4647
+ }
4648
+ .mobile-menu-fallback {
4649
+ display: none;
4650
+ }
4651
+ body.mobile-menu-open[data-view='files'] .mobile-menu-fallback {
4652
+ display: flex;
4653
+ position: fixed;
4654
+ inset: 0;
4655
+ bottom: var(--mobile-bottom-bar-height);
4656
+ z-index: 10000001;
4521
4657
  flex-direction: column;
4522
- align-items: stretch;
4523
- border-left: 0;
4524
- border-top: 1px solid rgba(0,0,0,0.08);
4525
- margin-top: auto;
4526
- width: 100%;
4527
- padding: 0 12px calc(12px + env(safe-area-inset-bottom));
4528
- background: transparent;
4529
- box-sizing: border-box;
4530
- gap: 12px;
4658
+ background: #f2f2f7;
4531
4659
  }
4532
- body.dark.mobile-menu-open .appcanvas > aside .menu-actions {
4533
- border-top-color: rgba(255,255,255,0.08);
4534
- background: transparent;
4660
+ body.dark.mobile-menu-open[data-view='files'] .mobile-menu-fallback {
4661
+ background: #0f1115;
4662
+ }
4663
+ .mobile-menu-fallback__header {
4664
+ display: flex;
4665
+ align-items: center;
4666
+ gap: 10px;
4667
+ padding: 12px;
4668
+ font-size: 15px;
4669
+ font-weight: 700;
4670
+ }
4671
+ .mobile-menu-fallback__logo {
4672
+ width: 22px;
4673
+ height: 22px;
4674
+ object-fit: contain;
4675
+ display: block;
4676
+ }
4677
+ .mobile-menu-fallback__body {
4678
+ flex: 1 1 auto;
4679
+ overflow-y: auto;
4680
+ padding: 0 12px 16px;
4681
+ box-sizing: border-box;
4535
4682
  }
4536
4683
  body.mobile-menu-open .appcanvas .container {
4537
4684
  display: none;
@@ -4629,7 +4776,7 @@ header.navheader .mode-selector .community-mode-toggle {
4629
4776
  })()
4630
4777
  </script>
4631
4778
  </head>
4632
- <body class='<%=theme%>' data-platform="<%=platform%>" data-agent="<%=agent%>">
4779
+ <body class='<%=theme%>' data-platform="<%=platform%>" data-agent="<%=agent%>" data-view="<%=type%>">
4633
4780
  <header class='navheader grabbable'>
4634
4781
  <h1>
4635
4782
  <a class='home' href="/home">
@@ -4700,6 +4847,32 @@ header.navheader .mode-selector .community-mode-toggle {
4700
4847
  <% } %>
4701
4848
  </div>
4702
4849
  <span class="disk-usage tab-metric__value" data-path="/" data-filepath="<%=path%>">--</span>
4850
+ <div class='mobile-sheet-actions' aria-label="Workspace actions">
4851
+ <a class='btn mobile-sheet-action' href="/home" aria-label="Home" title="Home">
4852
+ <i class="fa-solid fa-house"></i>
4853
+ </a>
4854
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#refresh-page" data-mobile-close-menu="true" aria-label="Refresh" title="Refresh">
4855
+ <i class="fa-solid fa-rotate-right"></i>
4856
+ </button>
4857
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#back" data-mobile-close-menu="true" aria-label="Back" title="Back">
4858
+ <i class="fa-solid fa-chevron-left"></i>
4859
+ </button>
4860
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#forward" data-mobile-close-menu="true" aria-label="Forward" title="Forward">
4861
+ <i class="fa-solid fa-chevron-right"></i>
4862
+ </button>
4863
+ <a class='btn mobile-sheet-action' href="/columns" aria-label="2 Columns" title="2 Columns">
4864
+ <i class="fa-solid fa-table-columns"></i>
4865
+ </a>
4866
+ <a class='btn mobile-sheet-action' href="/rows" aria-label="2 Rows" title="2 Rows">
4867
+ <i class="fa-solid fa-table-columns fa-rotate-270"></i>
4868
+ </a>
4869
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#new-window" data-mobile-close-menu="true" aria-label="New Window" title="New Window">
4870
+ <i class="fa-solid fa-plus"></i>
4871
+ </button>
4872
+ <button type='button' class='btn mobile-sheet-action mobile-close-window-action hidden' data-mobile-proxy="#close-window" data-mobile-close-menu="true" aria-label="Close Section" title="Close Section">
4873
+ <i class="fa-solid fa-xmark"></i>
4874
+ </button>
4875
+ </div>
4703
4876
  <div class='m n system' data-type="n">
4704
4877
  <%if (type==='browse') { %>
4705
4878
  <a id='devtab' data-mode="refresh" target="<%=dev_link%>" href="<%=dev_link%>" class="btn frame-link selected" data-index="10">
@@ -5022,6 +5195,63 @@ header.navheader .mode-selector .community-mode-toggle {
5022
5195
  </div>
5023
5196
  <% } %>
5024
5197
  </div>
5198
+ <nav class='mobile-bottom-nav' aria-label="Mobile navigation">
5199
+ <button type='button' class='btn2 mobile-bottom-nav__button mobile-nav-menu' data-mobile-proxy="#menu-mobile" aria-expanded="false" aria-label="Open Pinokio menu">
5200
+ <img class='mobile-bottom-nav__logo' src="/pinokio-black.png" alt="" aria-hidden="true">
5201
+ </button>
5202
+ <div class='mobile-bottom-nav__modes' role="tablist" aria-label="Workspace mode">
5203
+ <a class="mobile-mode-segment <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>" role="tab" data-mobile-mode="run" aria-selected="<%=type === 'run' ? 'true' : 'false'%>">Run</a>
5204
+ <a class="mobile-mode-segment <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>" role="tab" data-mobile-mode="browse" aria-selected="<%=type === 'browse' ? 'true' : 'false'%>">Dev</a>
5205
+ <a class="mobile-mode-segment <%=type === 'files' ? 'selected' : ''%>" href="<%=files_tab%>" role="tab" data-mobile-mode="files" aria-selected="<%=type === 'files' ? 'true' : 'false'%>">Files</a>
5206
+ </div>
5207
+ <% if (registryEnabled) { %>
5208
+ <button type='button' class='btn2 mobile-bottom-nav__button mobile-nav-community' data-mobile-proxy="#community-mode-toggle" data-mobile-close-menu="true" aria-pressed="false" aria-label="Community">
5209
+ <i class="fa-solid fa-globe"></i>
5210
+ </button>
5211
+ <% } %>
5212
+ <% if (type === 'run') { %>
5213
+ <button type='button' class='btn2 mobile-bottom-nav__button mobile-nav-ask-ai' data-mobile-proxy="#ask-ai-tab" data-mobile-close-menu="true" aria-pressed="false" aria-label="Ask AI">
5214
+ <i class="fa-solid fa-robot"></i>
5215
+ </button>
5216
+ <% } %>
5217
+ </nav>
5218
+ </div>
5219
+ <div class='mobile-menu-fallback' aria-hidden="true">
5220
+ <div class='mobile-menu-fallback__header'>
5221
+ <button type='button' class='btn2' data-mobile-proxy="#menu-mobile" aria-label="Close menu">
5222
+ <i class="fa-solid fa-chevron-left"></i>
5223
+ </button>
5224
+ <img class='mobile-menu-fallback__logo' src="/pinokio-black.png" alt="" aria-hidden="true">
5225
+ <div>Pinokio</div>
5226
+ </div>
5227
+ <div class='mobile-menu-fallback__body'>
5228
+ <div class='mobile-sheet-actions' aria-label="Workspace actions">
5229
+ <a class='btn mobile-sheet-action' href="/home" aria-label="Home" title="Home">
5230
+ <i class="fa-solid fa-house"></i>
5231
+ </a>
5232
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#refresh-page" data-mobile-close-menu="true" aria-label="Refresh" title="Refresh">
5233
+ <i class="fa-solid fa-rotate-right"></i>
5234
+ </button>
5235
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#back" data-mobile-close-menu="true" aria-label="Back" title="Back">
5236
+ <i class="fa-solid fa-chevron-left"></i>
5237
+ </button>
5238
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#forward" data-mobile-close-menu="true" aria-label="Forward" title="Forward">
5239
+ <i class="fa-solid fa-chevron-right"></i>
5240
+ </button>
5241
+ <a class='btn mobile-sheet-action' href="/columns" aria-label="2 Columns" title="2 Columns">
5242
+ <i class="fa-solid fa-table-columns"></i>
5243
+ </a>
5244
+ <a class='btn mobile-sheet-action' href="/rows" aria-label="2 Rows" title="2 Rows">
5245
+ <i class="fa-solid fa-table-columns fa-rotate-270"></i>
5246
+ </a>
5247
+ <button type='button' class='btn mobile-sheet-action' data-mobile-proxy="#new-window" data-mobile-close-menu="true" aria-label="New Window" title="New Window">
5248
+ <i class="fa-solid fa-plus"></i>
5249
+ </button>
5250
+ <button type='button' class='btn mobile-sheet-action mobile-close-window-action hidden' data-mobile-proxy="#close-window" data-mobile-close-menu="true" aria-label="Close Section" title="Close Section">
5251
+ <i class="fa-solid fa-xmark"></i>
5252
+ </button>
5253
+ </div>
5254
+ </div>
5025
5255
  </div>
5026
5256
  <script>
5027
5257
  let started_script
@@ -5186,6 +5416,20 @@ header.navheader .mode-selector .community-mode-toggle {
5186
5416
  }
5187
5417
  return `${base}:url`
5188
5418
  }
5419
+ const forceDefaultSelectionStorageKey = () => {
5420
+ const base = frameContextKey()
5421
+ if (!base) {
5422
+ const pagePath = (() => {
5423
+ try {
5424
+ return window.location?.pathname || ""
5425
+ } catch (_) {
5426
+ return ""
5427
+ }
5428
+ })()
5429
+ return `__pinokio_force_default_selection__:${pagePath}`
5430
+ }
5431
+ return `${base}:force-default-selection`
5432
+ }
5189
5433
  const SELECTION_SELECTOR_ATTRS = [
5190
5434
  'target',
5191
5435
  'data-target-full',
@@ -5253,6 +5497,52 @@ header.navheader .mode-selector .community-mode-toggle {
5253
5497
  storage.setItem(key, JSON.stringify(payload))
5254
5498
  } catch (_) {}
5255
5499
  }
5500
+ const clearPersistedFrameLinkSelection = () => {
5501
+ const storage = getWindowStorage()
5502
+ if (!storage) {
5503
+ return
5504
+ }
5505
+ try {
5506
+ const key = selectionStorageKey()
5507
+ if (key) {
5508
+ storage.removeItem(key)
5509
+ }
5510
+ } catch (_) {}
5511
+ try {
5512
+ const urlKey = selectionUrlStorageKey()
5513
+ if (urlKey) {
5514
+ storage.removeItem(urlKey)
5515
+ }
5516
+ } catch (_) {}
5517
+ }
5518
+ const markForceDefaultSelection = () => {
5519
+ const storage = getWindowStorage()
5520
+ const key = forceDefaultSelectionStorageKey()
5521
+ if (!storage || !key) {
5522
+ return
5523
+ }
5524
+ try {
5525
+ storage.setItem(key, "1")
5526
+ } catch (_) {}
5527
+ }
5528
+ const consumeForceDefaultSelection = () => {
5529
+ const storage = getWindowStorage()
5530
+ const key = forceDefaultSelectionStorageKey()
5531
+ if (!storage || !key) {
5532
+ return false
5533
+ }
5534
+ try {
5535
+ const value = storage.getItem(key)
5536
+ if (value !== "1") {
5537
+ return false
5538
+ }
5539
+ storage.removeItem(key)
5540
+ return true
5541
+ } catch (_) {
5542
+ return false
5543
+ }
5544
+ }
5545
+ let forceDefaultSelection = consumeForceDefaultSelection()
5256
5546
  const restorePersistedFrameLink = (providedPayload = null) => {
5257
5547
  const storage = getWindowStorage()
5258
5548
  const key = selectionStorageKey()
@@ -6053,14 +6343,14 @@ const rerenderMenuSection = (container, html) => {
6053
6343
  persistedSelectionRaw = null
6054
6344
  }
6055
6345
  const originalHasPersistedSelection = Boolean(persistedSelectionPayload)
6056
- const skipPersistedSelection = ignorePersistedSelection
6346
+ const triggeredByUser = Boolean(eventParam || explicitTarget)
6347
+ const followCurrentDefault = forceDefaultSelection && !triggeredByUser
6348
+ const skipPersistedSelection = ignorePersistedSelection || followCurrentDefault
6057
6349
  let hasPersistedSelection = skipPersistedSelection ? false : originalHasPersistedSelection
6058
6350
  if (skipPersistedSelection) {
6059
6351
  persistedSelectionRaw = null
6060
6352
  persistedSelectionPayload = null
6061
6353
  }
6062
-
6063
- const triggeredByUser = Boolean(eventParam || explicitTarget)
6064
6354
  let resolvedByGlobalSelector = false
6065
6355
 
6066
6356
  let target = explicitTarget
@@ -6109,6 +6399,13 @@ const rerenderMenuSection = (container, html) => {
6109
6399
  }
6110
6400
  }
6111
6401
 
6402
+ if (!target && followCurrentDefault) {
6403
+ const defaultSelection = getDefaultSelection()
6404
+ if (defaultSelection) {
6405
+ target = defaultSelection
6406
+ }
6407
+ }
6408
+
6112
6409
  const devTab = document.querySelector('#devtab.frame-link')
6113
6410
  if (!triggeredByUser && !resolvedByGlobalSelector && !target && devRouteActive && devTab) {
6114
6411
  const defaultCandidate = getDefaultSelection()
@@ -6197,7 +6494,12 @@ const rerenderMenuSection = (container, html) => {
6197
6494
  if (skipPersistedSelection && target.hasAttribute('data-default')) {
6198
6495
  ignorePersistedSelection = false
6199
6496
  }
6200
- persistFrameLinkSelection(target)
6497
+ if (forceDefaultSelection && triggeredByUser) {
6498
+ forceDefaultSelection = false
6499
+ }
6500
+ if (!followCurrentDefault) {
6501
+ persistFrameLinkSelection(target)
6502
+ }
6201
6503
 
6202
6504
  // save target.href
6203
6505
  <% if (type !== "run") { %>
@@ -6515,22 +6817,62 @@ const rerenderMenuSection = (container, html) => {
6515
6817
 
6516
6818
  const mobileMenuQuery = window.matchMedia ? window.matchMedia("(max-width: 768px)") : null
6517
6819
  const isMobileMenuOpen = () => document.body.classList.contains("mobile-menu-open")
6820
+ const appcanvas = document.querySelector(".appcanvas")
6821
+ const closeWindowButton = document.querySelector("#close-window")
6822
+ const syncMobileBottomNavState = () => {
6823
+ const mobileMenuOpen = isMobileMenuOpen()
6824
+ const communityModeActive = !!(appcanvas && appcanvas.classList.contains("community-mode"))
6825
+ const askAiOpen = !!(appcanvas && appcanvas.classList.contains("panel-open"))
6826
+ const mobileMenuFallback = document.querySelector(".mobile-menu-fallback")
6827
+ const currentView = document.body.getAttribute("data-view")
6828
+ const closeSectionVisible = !!(closeWindowButton && !closeWindowButton.classList.contains("hidden"))
6829
+
6830
+ document.querySelectorAll(".mobile-nav-menu").forEach((button) => {
6831
+ button.classList.toggle("selected", mobileMenuOpen)
6832
+ button.setAttribute("aria-expanded", mobileMenuOpen ? "true" : "false")
6833
+ })
6834
+ document.querySelectorAll(".mobile-nav-community").forEach((button) => {
6835
+ button.classList.toggle("selected", communityModeActive)
6836
+ button.setAttribute("aria-pressed", communityModeActive ? "true" : "false")
6837
+ })
6838
+ document.querySelectorAll(".mobile-nav-ask-ai").forEach((button) => {
6839
+ button.classList.toggle("selected", askAiOpen)
6840
+ button.setAttribute("aria-pressed", askAiOpen ? "true" : "false")
6841
+ })
6842
+ document.querySelectorAll(".mobile-mode-segment").forEach((segment) => {
6843
+ const segmentMode = segment.getAttribute("data-mobile-mode")
6844
+ const active = !communityModeActive && segmentMode === currentView
6845
+ segment.classList.toggle("selected", active)
6846
+ segment.setAttribute("aria-selected", active ? "true" : "false")
6847
+ })
6848
+ document.querySelectorAll(".mobile-close-window-action").forEach((button) => {
6849
+ button.classList.toggle("hidden", !closeSectionVisible)
6850
+ button.setAttribute("aria-hidden", closeSectionVisible ? "false" : "true")
6851
+ })
6852
+ if (mobileMenuFallback) {
6853
+ const showFallback = mobileMenuOpen && document.body.getAttribute("data-view") === "files"
6854
+ mobileMenuFallback.setAttribute("aria-hidden", showFallback ? "false" : "true")
6855
+ }
6856
+ }
6518
6857
  const setMobileMenuOpen = (next, options = {}) => {
6519
6858
  const shouldOpen = !!next
6520
6859
  if (!mobileMenuQuery || !mobileMenuQuery.matches) {
6521
6860
  document.body.classList.remove("mobile-menu-open")
6861
+ syncMobileBottomNavState()
6522
6862
  return
6523
6863
  }
6524
6864
  document.body.classList.toggle("mobile-menu-open", shouldOpen)
6525
6865
  if (shouldOpen && !options.skipRefresh) {
6526
6866
  refresh(false, { nodelay: true })
6527
6867
  }
6868
+ syncMobileBottomNavState()
6528
6869
  }
6529
6870
 
6530
6871
  const handleMobileMenuQueryChange = () => {
6531
6872
  if (!mobileMenuQuery || !mobileMenuQuery.matches) {
6532
6873
  document.body.classList.remove("mobile-menu-open")
6533
6874
  }
6875
+ syncMobileBottomNavState()
6534
6876
  }
6535
6877
  if (mobileMenuQuery && typeof mobileMenuQuery.addEventListener === "function") {
6536
6878
  mobileMenuQuery.addEventListener("change", handleMobileMenuQueryChange)
@@ -6553,6 +6895,43 @@ const rerenderMenuSection = (container, html) => {
6553
6895
  setMobileMenuOpen(false, { skipRefresh: true })
6554
6896
  })
6555
6897
  }
6898
+ document.querySelectorAll("[data-mobile-proxy]").forEach((trigger) => {
6899
+ trigger.addEventListener("click", (event) => {
6900
+ const selector = trigger.getAttribute("data-mobile-proxy")
6901
+ if (!selector) {
6902
+ return
6903
+ }
6904
+ const target = document.querySelector(selector)
6905
+ if (!target) {
6906
+ return
6907
+ }
6908
+ event.preventDefault()
6909
+ if (trigger.getAttribute("data-mobile-close-menu") === "true" && isMobileMenuOpen()) {
6910
+ setMobileMenuOpen(false, { skipRefresh: true })
6911
+ }
6912
+ target.click()
6913
+ setTimeout(syncMobileBottomNavState, 0)
6914
+ })
6915
+ })
6916
+ if (appcanvas && typeof MutationObserver === "function") {
6917
+ const mobileNavObserver = new MutationObserver(() => {
6918
+ syncMobileBottomNavState()
6919
+ })
6920
+ mobileNavObserver.observe(appcanvas, {
6921
+ attributes: true,
6922
+ attributeFilter: ["class"]
6923
+ })
6924
+ }
6925
+ if (closeWindowButton && typeof MutationObserver === "function") {
6926
+ const closeWindowObserver = new MutationObserver(() => {
6927
+ syncMobileBottomNavState()
6928
+ })
6929
+ closeWindowObserver.observe(closeWindowButton, {
6930
+ attributes: true,
6931
+ attributeFilter: ["class"]
6932
+ })
6933
+ }
6934
+ syncMobileBottomNavState()
6556
6935
  if (document.querySelector("#menu")) {
6557
6936
  document.querySelector("#menu").addEventListener("click", async (e) => {
6558
6937
  document.querySelector("aside").classList.toggle("hidden")
@@ -7631,6 +8010,13 @@ const rerenderMenuSection = (container, html) => {
7631
8010
  if (event.data.type === 'stream') {
7632
8011
  const frameName = resolveFrameName(null, event.source)
7633
8012
  updateTabTimestamp(frameName, Date.now())
8013
+ } else if (event.data.type === 'restart') {
8014
+ <% if (type === 'run') { %>
8015
+ clearPersistedFrameLinkSelection()
8016
+ markForceDefaultSelection()
8017
+ window.location.reload()
8018
+ <% } %>
8019
+ return
7634
8020
  } else if (event.data.type === 'result') {
7635
8021
  refresh()
7636
8022
  refresh_du()
@@ -856,6 +856,12 @@ document.addEventListener("DOMContentLoaded", async () => {
856
856
  }
857
857
  n.Noty(payload)
858
858
  }
859
+ } else if (packet.type === "restart") {
860
+ try {
861
+ if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
862
+ window.parent.postMessage(packet, "*")
863
+ }
864
+ } catch (_) {}
859
865
  } else if (packet.type === "result") {
860
866
  // this.socket.close()
861
867
  } else if (packet.type === "info") {
@@ -1422,6 +1422,12 @@ document.addEventListener("DOMContentLoaded", async () => {
1422
1422
  }
1423
1423
  n.Noty(payload)
1424
1424
  }
1425
+ } else if (packet.type === "restart") {
1426
+ try {
1427
+ if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
1428
+ window.parent.postMessage(packet, "*")
1429
+ }
1430
+ } catch (_) {}
1425
1431
  } else if (packet.type === "result") {
1426
1432
  if (packet.id === "terminal.upload") {
1427
1433
  const uploaded = Array.isArray(packet.data && packet.data.files) ? packet.data.files : []