pinokiod 7.5.6 → 7.5.8

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.
@@ -0,0 +1,647 @@
1
+ const path = require('path')
2
+ const unparse = require('yargs-unparser-custom-flag')
3
+
4
+ const SHELL_RUN_GUARD = Symbol('pinokio.shellRunCondaRuntimeGuard')
5
+ const noticeSessions = new Set()
6
+
7
+ const MUTATING_COMMANDS = new Set([
8
+ 'create',
9
+ 'install',
10
+ 'remove',
11
+ 'uninstall',
12
+ 'update',
13
+ 'upgrade',
14
+ ])
15
+
16
+ const ENV_MUTATING_COMMANDS = new Set([
17
+ 'create',
18
+ 'remove',
19
+ 'update',
20
+ ])
21
+
22
+ const PROTECTED_PACKAGES = new Set([
23
+ 'conda',
24
+ 'python',
25
+ 'conda-libmamba-solver',
26
+ ])
27
+
28
+ const VALUE_FLAGS = new Set([
29
+ '-c',
30
+ '--channel',
31
+ '-f',
32
+ '--file',
33
+ '-n',
34
+ '--name',
35
+ '-p',
36
+ '--prefix',
37
+ '--solver',
38
+ ])
39
+
40
+ function pathApi(platform) {
41
+ return platform === 'win32' ? path.win32 : path.posix
42
+ }
43
+
44
+ function normalizePathname(value, platform) {
45
+ if (!value || typeof value !== 'string') {
46
+ return ''
47
+ }
48
+ const api = pathApi(platform)
49
+ const normalized = api.normalize(value)
50
+ return platform === 'win32' ? normalized.toLowerCase() : normalized
51
+ }
52
+
53
+ function isSamePath(target, root, platform) {
54
+ const normalizedTarget = normalizePathname(target, platform)
55
+ const normalizedRoot = normalizePathname(root, platform)
56
+ if (!normalizedTarget || !normalizedRoot) {
57
+ return false
58
+ }
59
+ return pathApi(platform).relative(normalizedRoot, normalizedTarget) === ''
60
+ }
61
+
62
+ function resolvePathFrom(basePath, value, platform) {
63
+ if (!value || typeof value !== 'string') {
64
+ return ''
65
+ }
66
+ const api = pathApi(platform)
67
+ if (api.isAbsolute(value)) {
68
+ return api.normalize(value)
69
+ }
70
+ return api.resolve(basePath || process.cwd(), value)
71
+ }
72
+
73
+ function usesBackslashEscapes(context = {}) {
74
+ const shellName = String(context.shellName || '').toLowerCase()
75
+ if (shellName.includes('cmd.exe') || shellName === 'cmd' || shellName.includes('powershell') || shellName.includes('pwsh')) {
76
+ return false
77
+ }
78
+ return context.platform !== 'win32'
79
+ }
80
+
81
+ function shellTokenize(input, context = {}) {
82
+ const tokens = []
83
+ let token = ''
84
+ let quote = null
85
+ let escaped = false
86
+ const backslashEscapes = usesBackslashEscapes(context)
87
+
88
+ const push = () => {
89
+ if (token.length > 0) {
90
+ tokens.push(token)
91
+ token = ''
92
+ }
93
+ }
94
+
95
+ for (const char of String(input || '')) {
96
+ if (escaped) {
97
+ token += char
98
+ escaped = false
99
+ continue
100
+ }
101
+ if (quote === "'") {
102
+ if (char === "'") {
103
+ quote = null
104
+ } else {
105
+ token += char
106
+ }
107
+ continue
108
+ }
109
+ if (quote === '"') {
110
+ if (char === '"') {
111
+ quote = null
112
+ } else if (char === '\\' && backslashEscapes) {
113
+ escaped = true
114
+ } else {
115
+ token += char
116
+ }
117
+ continue
118
+ }
119
+ if (char === "'" || char === '"') {
120
+ quote = char
121
+ continue
122
+ }
123
+ if (char === '\\' && backslashEscapes) {
124
+ escaped = true
125
+ continue
126
+ }
127
+ if (/\s/.test(char)) {
128
+ push()
129
+ continue
130
+ }
131
+ token += char
132
+ }
133
+ if (escaped) {
134
+ token += '\\'
135
+ }
136
+ push()
137
+ return { tokens, closed: quote === null }
138
+ }
139
+
140
+ function splitShellSegments(input, context = {}) {
141
+ const parts = []
142
+ let segment = ''
143
+ let quote = null
144
+ let escaped = false
145
+ const text = String(input || '')
146
+ const backslashEscapes = usesBackslashEscapes(context)
147
+
148
+ const pushSegment = () => {
149
+ parts.push({ type: 'segment', text: segment })
150
+ segment = ''
151
+ }
152
+
153
+ for (let i = 0; i < text.length; i++) {
154
+ const char = text[i]
155
+ const next = text[i + 1]
156
+
157
+ if (escaped) {
158
+ segment += char
159
+ escaped = false
160
+ continue
161
+ }
162
+ if (quote === "'") {
163
+ segment += char
164
+ if (char === "'") {
165
+ quote = null
166
+ }
167
+ continue
168
+ }
169
+ if (quote === '"') {
170
+ segment += char
171
+ if (char === '"') {
172
+ quote = null
173
+ } else if (char === '\\' && backslashEscapes) {
174
+ escaped = true
175
+ }
176
+ continue
177
+ }
178
+ if (char === "'" || char === '"') {
179
+ quote = char
180
+ segment += char
181
+ continue
182
+ }
183
+ if (char === '\\' && backslashEscapes) {
184
+ escaped = true
185
+ segment += char
186
+ continue
187
+ }
188
+ if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
189
+ pushSegment()
190
+ parts.push({ type: 'separator', text: char + next })
191
+ i++
192
+ continue
193
+ }
194
+ if (char === '&') {
195
+ const previous = text[i - 1]
196
+ if (previous === '>' || previous === '<' || next === '>') {
197
+ segment += char
198
+ continue
199
+ }
200
+ pushSegment()
201
+ parts.push({ type: 'unsafe-separator', text: char })
202
+ continue
203
+ }
204
+ if (char === '|') {
205
+ pushSegment()
206
+ parts.push({ type: 'unsafe-separator', text: char })
207
+ continue
208
+ }
209
+ if (char === ';' || char === '\n' || char === '\r') {
210
+ pushSegment()
211
+ parts.push({ type: 'separator', text: char })
212
+ continue
213
+ }
214
+ segment += char
215
+ }
216
+ pushSegment()
217
+ return parts
218
+ }
219
+
220
+ function getExecutableToken(tokens) {
221
+ let index = 0
222
+ while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {
223
+ index++
224
+ }
225
+ if (tokens[index] === '&' || String(tokens[index]).toLowerCase() === 'call') {
226
+ index++
227
+ }
228
+ return { executable: tokens[index] || '', index }
229
+ }
230
+
231
+ function executableBasename(executable, platform) {
232
+ const api = pathApi(platform)
233
+ return api.basename(String(executable || '')).toLowerCase()
234
+ }
235
+
236
+ function isCondaExecutable(executable, context) {
237
+ const platform = context.platform || process.platform
238
+ if (!executable) {
239
+ return false
240
+ }
241
+ if (String(executable).toLowerCase() === 'conda') {
242
+ return true
243
+ }
244
+ const base = executableBasename(executable, platform)
245
+ return base === 'conda' || base === 'conda.exe' || base === 'conda.bat' || base === 'conda.cmd'
246
+ }
247
+
248
+ function resolveWrapperTarget(params, context) {
249
+ const conda = params ? params.conda : undefined
250
+ const platform = context.platform || process.platform
251
+ const cwd = context.cwd || (params && params.path) || process.cwd()
252
+ if (!conda) {
253
+ return { kind: 'managed-base', source: 'wrapper', prefix: context.managedBasePrefix || '' }
254
+ }
255
+ if (typeof conda === 'string') {
256
+ return { kind: 'app-env', source: 'wrapper', prefix: resolvePathFrom(cwd, conda, platform) }
257
+ }
258
+ if (conda && conda.path) {
259
+ return { kind: 'app-env', source: 'wrapper', prefix: resolvePathFrom(cwd, conda.path, platform) }
260
+ }
261
+ if (conda && conda.name) {
262
+ if (conda.name === 'base') {
263
+ return { kind: 'managed-base', source: 'wrapper', prefix: context.managedBasePrefix || '' }
264
+ }
265
+ return {
266
+ kind: 'app-env',
267
+ source: 'wrapper',
268
+ name: conda.name,
269
+ }
270
+ }
271
+ return { kind: 'unknown', source: 'wrapper' }
272
+ }
273
+
274
+ function parseCommandTarget(tokens, executableIndex) {
275
+ for (let i = executableIndex + 1; i < tokens.length; i++) {
276
+ const token = tokens[i]
277
+ if (token === '-p' || token === '--prefix') {
278
+ return { type: 'prefix', value: tokens[i + 1] || '' }
279
+ }
280
+ if (token.startsWith('--prefix=')) {
281
+ return { type: 'prefix', value: token.slice('--prefix='.length) }
282
+ }
283
+ if (token === '-n' || token === '--name') {
284
+ return { type: 'name', value: tokens[i + 1] || '' }
285
+ }
286
+ if (token.startsWith('--name=')) {
287
+ return { type: 'name', value: token.slice('--name='.length) }
288
+ }
289
+ }
290
+ return null
291
+ }
292
+
293
+ function refineTargetFromCommandArgs(target, commandTarget, params, context) {
294
+ if (!commandTarget || !commandTarget.value) {
295
+ return target
296
+ }
297
+ const platform = context.platform || process.platform
298
+ const managedBasePrefix = context.managedBasePrefix || ''
299
+ if (commandTarget.type === 'name') {
300
+ if (commandTarget.value === 'base') {
301
+ return { kind: 'managed-base', source: 'command-target', name: 'base', prefix: managedBasePrefix }
302
+ }
303
+ return { kind: 'app-env', source: 'command-target', name: commandTarget.value }
304
+ }
305
+ const resolved = resolvePathFrom(context.cwd || (params && params.path) || process.cwd(), commandTarget.value, platform)
306
+ if (managedBasePrefix && isSamePath(resolved, managedBasePrefix, platform)) {
307
+ return { kind: 'managed-base', source: 'command-target', prefix: managedBasePrefix }
308
+ }
309
+ return { kind: 'app-env', source: 'command-target', prefix: resolved }
310
+ }
311
+
312
+ function resolveTarget(tokens, executableIndex, params, context) {
313
+ const wrapperTarget = resolveWrapperTarget(params, context)
314
+ return refineTargetFromCommandArgs(wrapperTarget, parseCommandTarget(tokens, executableIndex), params, context)
315
+ }
316
+
317
+ function findSubcommand(tokens, executableIndex) {
318
+ for (let i = executableIndex + 1; i < tokens.length; i++) {
319
+ const token = String(tokens[i] || '').toLowerCase()
320
+ if (!token || token.startsWith('-')) {
321
+ if (VALUE_FLAGS.has(token)) {
322
+ i++
323
+ }
324
+ continue
325
+ }
326
+ if (token === 'env') {
327
+ const subcommand = String(tokens[i + 1] || '').toLowerCase()
328
+ if (ENV_MUTATING_COMMANDS.has(subcommand)) {
329
+ return { name: `env ${subcommand}`, index: i, endIndex: i + 1, mutating: true }
330
+ }
331
+ return { name: `env ${subcommand}`, index: i, endIndex: i + 1, mutating: false }
332
+ }
333
+ if (MUTATING_COMMANDS.has(token)) {
334
+ return { name: token, index: i, endIndex: i, mutating: true }
335
+ }
336
+ return { name: token, index: i, endIndex: i, mutating: false }
337
+ }
338
+ return null
339
+ }
340
+
341
+ function isEnvironmentTargetingSubcommand(subcommand) {
342
+ return subcommand && (
343
+ subcommand.name === 'create'
344
+ || subcommand.name === 'env create'
345
+ || subcommand.name === 'env remove'
346
+ || subcommand.name === 'env update'
347
+ )
348
+ }
349
+
350
+ function normalizePackageSpec(token) {
351
+ if (!token || token.startsWith('-')) {
352
+ return ''
353
+ }
354
+ const scoped = String(token).split('::').pop()
355
+ return scoped.split(/[=<>!~]/)[0].trim().toLowerCase()
356
+ }
357
+
358
+ function hasAllFlag(tokens, startIndex) {
359
+ return tokens.slice(startIndex).some((token) => token === '--all')
360
+ }
361
+
362
+ function hasDryRunFlag(tokens, startIndex) {
363
+ return tokens.slice(startIndex).some((token) => token === '--dry-run')
364
+ }
365
+
366
+ function explicitPackageNames(tokens, startIndex) {
367
+ const packages = []
368
+ for (let i = startIndex; i < tokens.length; i++) {
369
+ const token = tokens[i]
370
+ if (!token) {
371
+ continue
372
+ }
373
+ if (token.startsWith('--') && token.includes('=')) {
374
+ continue
375
+ }
376
+ if (VALUE_FLAGS.has(token)) {
377
+ i++
378
+ continue
379
+ }
380
+ if (token.startsWith('-')) {
381
+ continue
382
+ }
383
+ const pkg = normalizePackageSpec(token)
384
+ if (pkg) {
385
+ packages.push(pkg)
386
+ }
387
+ }
388
+ return packages
389
+ }
390
+
391
+ function protectedPackageFromSubcommand(tokens, subcommand) {
392
+ const packages = explicitPackageNames(tokens, subcommand.endIndex + 1)
393
+ return packages.find((pkg) => PROTECTED_PACKAGES.has(pkg)) || ''
394
+ }
395
+
396
+ function isListedBroadMutation(tokens, subcommand, target) {
397
+ if ((subcommand.name === 'update' || subcommand.name === 'upgrade') && hasAllFlag(tokens, subcommand.endIndex + 1)) {
398
+ return true
399
+ }
400
+ return subcommand.name === 'env remove' && target.source === 'command-target'
401
+ }
402
+
403
+ function classifyCommandTokens(tokens, params = {}, context = {}) {
404
+ const { executable, index: executableIndex } = getExecutableToken(tokens)
405
+ if (!isCondaExecutable(executable, context)) {
406
+ return { shouldSkip: false }
407
+ }
408
+ const target = resolveTarget(tokens, executableIndex, params, context)
409
+ if (target.kind !== 'managed-base') {
410
+ return { shouldSkip: false }
411
+ }
412
+ const subcommand = findSubcommand(tokens, executableIndex)
413
+ if (!subcommand || !subcommand.mutating) {
414
+ return { shouldSkip: false }
415
+ }
416
+ if (hasDryRunFlag(tokens, subcommand.endIndex + 1)) {
417
+ return { shouldSkip: false }
418
+ }
419
+
420
+ if (isEnvironmentTargetingSubcommand(subcommand) && target.source !== 'command-target') {
421
+ return { shouldSkip: false }
422
+ }
423
+
424
+ const protectedPackage = protectedPackageFromSubcommand(tokens, subcommand)
425
+ if (!protectedPackage && !isListedBroadMutation(tokens, subcommand, target)) {
426
+ return { shouldSkip: false }
427
+ }
428
+
429
+ return {
430
+ shouldSkip: true,
431
+ target,
432
+ reason: protectedPackage
433
+ ? `protected base Conda package: ${protectedPackage}`
434
+ : `listed broad managed-base mutation: ${subcommand.name}`,
435
+ }
436
+ }
437
+
438
+ function noopCommand() {
439
+ return 'echo Pinokio skipped a Conda setup command. Pinokio is continuing.'
440
+ }
441
+
442
+ function classifySegment(segment, params, context) {
443
+ const parsed = shellTokenize(segment, context)
444
+ if (!parsed.closed) {
445
+ return { shouldSkip: false }
446
+ }
447
+ return {
448
+ ...classifyCommandTokens(parsed.tokens, params, context),
449
+ tokens: parsed.tokens,
450
+ }
451
+ }
452
+
453
+ function rewriteStringMessage(message, params, context) {
454
+ const parts = splitShellSegments(message, context)
455
+ const skipped = []
456
+ const hasUnsafeSeparator = parts.some((part) => part.type === 'unsafe-separator')
457
+
458
+ if (hasUnsafeSeparator) {
459
+ for (const part of parts) {
460
+ if (part.type !== 'segment') {
461
+ continue
462
+ }
463
+ const body = part.text.trim()
464
+ if (!body) {
465
+ continue
466
+ }
467
+ const classification = classifySegment(body, params, context)
468
+ if (classification.shouldSkip) {
469
+ skipped.push({
470
+ command: body,
471
+ reason: classification.reason,
472
+ target: classification.target,
473
+ })
474
+ }
475
+ }
476
+ if (skipped.length > 0) {
477
+ return { message: noopCommand(), skipped }
478
+ }
479
+ return { message, skipped }
480
+ }
481
+
482
+ const rewritten = parts.map((part) => {
483
+ if (part.type !== 'segment') {
484
+ return part.text
485
+ }
486
+ const leading = (part.text.match(/^\s*/) || [''])[0]
487
+ const trailing = (part.text.match(/\s*$/) || [''])[0]
488
+ const body = part.text.trim()
489
+ if (!body) {
490
+ return part.text
491
+ }
492
+ const classification = classifySegment(body, params, context)
493
+ if (!classification.shouldSkip) {
494
+ return part.text
495
+ }
496
+ skipped.push({
497
+ command: body,
498
+ reason: classification.reason,
499
+ target: classification.target,
500
+ })
501
+ return leading + noopCommand() + trailing
502
+ }).join('')
503
+ return { message: rewritten, skipped }
504
+ }
505
+
506
+ function structuredTokens(message) {
507
+ if (!message || message.constructor !== Object) {
508
+ return null
509
+ }
510
+ return unparse(message)
511
+ .filter((item) => item != null)
512
+ .map((item) => String(item))
513
+ }
514
+
515
+ function rewriteSingleMessage(message, params, context) {
516
+ if (typeof message === 'string') {
517
+ return rewriteStringMessage(message, params, context)
518
+ }
519
+ const tokens = structuredTokens(message)
520
+ if (!tokens) {
521
+ return { message, skipped: [] }
522
+ }
523
+ const classification = classifyCommandTokens(tokens, params, context)
524
+ if (!classification.shouldSkip) {
525
+ return { message, skipped: [] }
526
+ }
527
+ return {
528
+ message: noopCommand(),
529
+ skipped: [{
530
+ command: tokens.join(' '),
531
+ reason: classification.reason,
532
+ target: classification.target,
533
+ }],
534
+ }
535
+ }
536
+
537
+ function sessionKey(params, context) {
538
+ if (context.sessionKey) {
539
+ return context.sessionKey
540
+ }
541
+ const parent = params && params.$parent
542
+ return (parent && (parent.id || parent.path)) || (params && (params.group || params.id || params.path)) || 'global'
543
+ }
544
+
545
+ function escapeHtml(value) {
546
+ return String(value)
547
+ .replace(/&/g, '&amp;')
548
+ .replace(/</g, '&lt;')
549
+ .replace(/>/g, '&gt;')
550
+ .replace(/"/g, '&quot;')
551
+ .replace(/'/g, '&#39;')
552
+ }
553
+
554
+ function formatRawNotice(skip) {
555
+ const target = skip.target && skip.target.kind ? skip.target.kind : 'unknown'
556
+ return [
557
+ '',
558
+ '[Pinokio] Skipped Conda setup command.',
559
+ `[Pinokio] Command: ${JSON.stringify(skip.command)}`,
560
+ "[Pinokio] The command targets Pinokio's protected base Conda setup.",
561
+ `[Pinokio] Reason: ${skip.reason || 'protected base Conda setup'}`,
562
+ `[Pinokio] Target: ${target}`,
563
+ '',
564
+ ].join('\r\n')
565
+ }
566
+
567
+ function formatNotifyHtml(skipped) {
568
+ const count = skipped.length
569
+ const firstCommand = skipped[0] && skipped[0].command ? String(skipped[0].command) : ''
570
+ const truncatedCommand = firstCommand.length > 240 ? `${firstCommand.slice(0, 237)}...` : firstCommand
571
+ const title = count === 1
572
+ ? 'Pinokio skipped a Conda setup command.'
573
+ : `Pinokio skipped ${count} Conda setup commands.`
574
+ const commandHtml = truncatedCommand
575
+ ? [
576
+ '<pre style="white-space:pre-wrap;margin:8px 0 0;font-size:12px;line-height:1.35;">',
577
+ escapeHtml(truncatedCommand),
578
+ '</pre>',
579
+ ].join('')
580
+ : ''
581
+ const moreHtml = count > 1
582
+ ? `<br>${escapeHtml(`${count - 1} more skipped command${count === 2 ? ' is' : 's are'} listed in the terminal log.`)}`
583
+ : ''
584
+ return [
585
+ `<b>${escapeHtml(title)}</b>`,
586
+ escapeHtml('Pinokio already manages base Conda.'),
587
+ escapeHtml('Pinokio is continuing.'),
588
+ commandHtml,
589
+ `${escapeHtml('Details are in the terminal/log.')}${moreHtml}`,
590
+ ].filter(Boolean).join('<br>')
591
+ }
592
+
593
+ function emitSkipNotices(params, skipped, context) {
594
+ if (!skipped.length || typeof context.ondata !== 'function') {
595
+ return
596
+ }
597
+ for (const skip of skipped) {
598
+ context.ondata({ raw: formatRawNotice(skip) })
599
+ }
600
+ const key = sessionKey(params, context)
601
+ if (noticeSessions.has(key)) {
602
+ return
603
+ }
604
+ noticeSessions.add(key)
605
+ context.ondata({
606
+ silent: true,
607
+ type: 'warning',
608
+ html: formatNotifyHtml(skipped),
609
+ }, 'notify')
610
+ }
611
+
612
+ function applyCondaRuntimeGuard(params, context = {}) {
613
+ if (!params || !params[SHELL_RUN_GUARD]) {
614
+ return { params, skipped: [] }
615
+ }
616
+ if (params.conda && params.conda.skip === true) {
617
+ return { params, skipped: [] }
618
+ }
619
+ const message = params.message
620
+ if (typeof message === 'undefined' || message === null) {
621
+ return { params, skipped: [] }
622
+ }
623
+ const allSkipped = []
624
+ if (Array.isArray(message)) {
625
+ params.message = message.map((item) => {
626
+ const result = rewriteSingleMessage(item, params, context)
627
+ allSkipped.push(...result.skipped)
628
+ return result.message
629
+ })
630
+ } else {
631
+ const result = rewriteSingleMessage(message, params, context)
632
+ params.message = result.message
633
+ allSkipped.push(...result.skipped)
634
+ }
635
+ emitSkipNotices(params, allSkipped, context)
636
+ return { params, skipped: allSkipped }
637
+ }
638
+
639
+ function resetNoticeSessionsForTest() {
640
+ noticeSessions.clear()
641
+ }
642
+
643
+ module.exports = {
644
+ SHELL_RUN_GUARD,
645
+ applyCondaRuntimeGuard,
646
+ resetNoticeSessionsForTest,
647
+ }
package/kernel/shells.js CHANGED
@@ -497,7 +497,7 @@ class Shells {
497
497
  return ""
498
498
  }
499
499
 
500
- let response = await sh.start(params, async (stream) => {
500
+ let response = await sh.start(params, async (stream, type) => {
501
501
  /*
502
502
  {
503
503
  method: "shell.run",
@@ -521,6 +521,12 @@ class Shells {
521
521
  }
522
522
  */
523
523
  try {
524
+ if (type) {
525
+ if (ondata) {
526
+ ondata(stream, type)
527
+ }
528
+ return
529
+ }
524
530
  const normalizedChunk = normalizeLiveEventChunk(stream.raw)
525
531
  if (normalizedChunk.length > 0) {
526
532
  liveEventBuffer = (liveEventBuffer + normalizedChunk).slice(-300)
@@ -601,7 +607,7 @@ class Shells {
601
607
  liveEventHandlersClosed = true
602
608
  break
603
609
  }
604
- if (isBreakHandler && liveErrors.size > 0) {
610
+ if (isBreakHandler && params.input !== true && liveErrors.size > 0) {
605
611
  sh.continue(liveEventBuffer)
606
612
  liveEventHandlersClosed = true
607
613
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.6",
3
+ "version": "7.5.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -4776,6 +4776,7 @@ class Server {
4776
4776
  await fs.promises.mkdir(candidate, { recursive: true })
4777
4777
  return {
4778
4778
  logsRoot: candidate,
4779
+ workspacePath,
4779
4780
  displayPath: this.formatLogsDisplayPath(candidate),
4780
4781
  title: normalized
4781
4782
  }
@@ -8142,6 +8143,7 @@ class Server {
8142
8143
  logsDraftUrl: draftUrl,
8143
8144
  logsEmbedded: embedded,
8144
8145
  logsInitialView: initialView,
8146
+ logsWorkspaceCwd: '',
8145
8147
  })
8146
8148
  return
8147
8149
  }
@@ -8162,6 +8164,7 @@ class Server {
8162
8164
  logsDraftUrl: draftUrl,
8163
8165
  logsEmbedded: embedded,
8164
8166
  logsInitialView: initialView,
8167
+ logsWorkspaceCwd: context.workspacePath || '',
8165
8168
  })
8166
8169
  }))
8167
8170
  this.app.get("/api/logs/tree", ex(async (req, res) => {