Skip to content

Push Workspace Files to an Object #139

Description

@Rider-Linden

Implementation Plan: Push Workspace Files to an Object

This document defines the design and implementation for writing a selection of local
workspace files into the inventory of a published in-world prim.

Tracking issue: split from secondlife/sl-vscode-plugin#130 ("push" half).

Related documents:


Overview

Today the only way to get a local file into an object is to open the in-world item and
save over it, one item at a time, and there is no way at all to create an item from a
local file without first creating it by hand in the viewer.

Push takes a multi-file selection from the VS Code explorer and writes it into a
single prim of a published object: updating items that already exist, creating those
that do not, and linking the results.

Push is the mutating half of #130 and is treated accordingly. It never deletes, it
confirms before it acts, and it reports exactly what it did.


Goals

  • Write an explicit selection of workspace files into one prim, in one action.
  • Create items that do not exist yet; update those that do.
  • Derive everything from the user's selection rather than inferring intent.
  • State the target unambiguously before any mutation occurs.
  • Report per-file outcomes, including partial failures.
  • Link created and updated items to their source files.

Non-Goals

  • Deleting or pruning in-world items that have no local counterpart.
  • Pushing to more than one prim in a single operation.
  • Rollback or any transactional guarantee.
  • Comparing local and in-world content before overwriting (deferred; see
    Future Work).
  • Controlling the run state of created scripts.
  • A manifest or any persisted, repeatable deployment definition.

Background

Why push is harder than pull

Pull's input is an object: fully enumerable and unambiguous. Push's input is "some
files", and every property push needs — is this a deliverable or a module, what should
it be called in-world, which prim does it belong in, does it already exist — has to be
inferred from a directory listing.

Explicit multi-selection removes most of that. Because the user names the exact
files, push does not need an inclusion heuristic, does not need to exclude configured
include paths, and does not need to distinguish a deliverable script from a required
module. Whatever is selected is what gets pushed.

Protocol constraints that shape the design

From Message_Interfaces.md:

Constraint Consequence
object.item.create carries no script source Every new script is create → object.content.save, two round trips plus a compile.
Only one object.item.create may be in flight per prim (-32600) Creation is strictly serial. Targeting a single prim makes this automatic.
30s timeout on create, 60s on script save and compile A large push takes minutes, not seconds.
vm is required on object.item.create for scripts Push cannot defer VM choice to the viewer when creating.
vm is optional on object.content.save Push can and does defer VM choice to the viewer when updating.
Omitting running on object.content.save preserves run state Updates never disturb a running script.
object.item.create accepts text for notecards New notecards are a single round trip.
Writes require modify on both the item and the containing prim Validate before starting.
An object must be published before its items are addressable Validate before starting.

Design Decisions

Settled during design review and recorded so the rationale is not lost.

# Decision Rationale
1 Input is an explicit multi-file selection in the VS Code explorer. Dissolves the "which files are deliverables" problem entirely. No heuristic can distinguish a module from a script reliably; the user can.
2 One command, contributed to explorer/context and hidden from the Command Palette. The selection array is only delivered to commands invoked from the explorer context menu. A palette invocation has no selection and no API exists to read one.
3 The command is visible whenever connected and enabled only when the SL explorer has a focused object. A greyed item tells the user the feature exists and what is missing; a hidden one reads as "not implemented".
4 The target prim is derived from the SL explorer's focused row. The webview already tracks a single focused row. Object row → root prim, linked prim row → that prim, item row → its containing prim. Removes both picker steps from the common flow.
5 Exactly one target prim per push. Prim identity cannot be inferred from a folder layout — link numbers are unstable across relinks and names are not unique. Targeting one explicitly chosen prim removes the problem rather than pretending to solve it.
6 Item type is derived from the file extension, reusing typeAndVmFromExtension. The convention already exists and drives the explorer's "New file…" flow. Push introduces no second rule.
7 Scripts lose their synthetic extension; notecards keep their filename verbatim, extension included. Symmetric with pull decision 10, so a pull → push round trip is name-stable.
8 New LSL scripts default to mono. Mono is the default VM for LSL. typeAndVmFromExtension currently returns lsl2; that is corrected in the shared helper rather than overridden locally.
9 vm is omitted when updating and sent only when creating. The viewer infers and preserves the existing VM on save. On create the protocol requires a value.
10 Existing items are matched by comparing sanitise(displayName(item)) with the local filename. Pull's sanitisation is lossy — an item named foo/bar becomes foo_bar.lsl. Matching in the sanitised domain recovers the correspondence with no persistence.
11 Duplicate target names abort the entire push before anything is written. Two selected files resolving to the same in-world name is unresolvable, and silently letting the second overwrite the first would be worse than refusing.
12 Folders in the selection expand exactly one level. Recursive expansion re-introduces the "I just pushed my whole include directory" problem that explicit selection was chosen to avoid.
13 Created scripts run as the simulator creates them. Run-state control costs two extra round trips per script. Deferred; see Known Limitations.
14 Confirmation lists dispositions but does not compare content. Disposition comes from the inventory snapshot and is metadata-only. Content comparison would require fetching every matching item before anything happens. Deferred.
15 Push never deletes. There is no prune, and there is no cleanup of a created item whose content save failed.

Detailed Design

Command and surfaces

  • Id: slVscodeEdit.pushFilesToObject
  • Title: Second Life: Push to Object...
  • Invocation: right-click on a file selection in the VS Code explorer.
"commands": [
  {
    "command": "slVscodeEdit.pushFilesToObject",
    "title": "Push to Object...",
    "category": "Second Life",
    "enablement": "slVscodeEdit:objectSelected"
  }
],
"menus": {
  "commandPalette": [
    { "command": "slVscodeEdit.pushFilesToObject", "when": "false" }
  ],
  "explorer/context": [
    {
      "command": "slVscodeEdit.pushFilesToObject",
      "when": "resourceScheme == file && slVscodeEdit:connected",
      "group": "navigation@100"
    }
  ]
}

The handler signature receives the clicked resource and the full selection:

async (clicked: vscode.Uri, selection?: vscode.Uri[]) => {
    const uris = selection?.length ? selection : clicked ? [clicked] : [];
    ...
}

selection is in explorer display order, not click order, and can be undefined on
some invocation paths — hence the fallback. Hiding the command from the palette does
not make it unreachable (a user keybinding invokes it with no arguments at all), so the
empty-selection guard is required regardless.

Target selection

The SL explorer is a webview ("type": "webview", slInworldExplorer), so VS Code has
no built-in notion of selection in it. The webview already maintains a single focused
row in state.focusedId, set by setFocus() in
src/webview/explorer/explorer.ts on both click
and keyboard navigation.

  1. setFocus() posts the focused row's identity to the extension.
  2. src/vscode/objectexplorerwebview.ts records
    the resolved target and sets the context key:
    vscode.commands.executeCommand("setContext", "slVscodeEdit:objectSelected", Boolean(target));
  3. The key is cleared on disconnect, on unpublish of the focused object, and when a
    refresh leaves the focused row nonexistent.

Target resolution:

Focused row Target prim
Object Root prim
Linked prim That prim
Inventory item The prim containing it

If the focus cannot be resolved to a prim at push time — because the object was
unpublished between focus and invocation — fall back to a quick pick of published
objects followed by a quick pick of prims.

Push set construction

  1. Start from the selected URIs.
  2. Expand any directory one level, taking only files. Nested directories are
    ignored and counted.
  3. Drop files that do not decode as UTF-8; report them as skipped.
  4. Warn, and require confirmation, for any file above a size threshold.
  5. For each remaining file, derive type, VM and target name:
    • typeAndVmFromExtension(ext) from
      src/vscode/objectcontentprovider.ts
      gives { type, vm }. .luau → script/luau, .lsl → script/mono, anything
      else → notecard.
    • Scripts: target name is the filename with the synthetic extension removed.
    • Notecards: target name is the filename verbatim, extension included.
  6. Collision check. If any two entries resolve to the same target name, abort the
    whole push and show which files collided. Nothing is written.

Matching against existing inventory

Take a snapshot of the target prim's inventory. For each push entry, compare its target
name with sanitise(displayName(item)) for every item in that prim, using the same
sanitisation helper introduced by pull.

  • Match → update via object.content.save.
  • No match → create via object.item.create, then object.content.save.

Matching in the sanitised domain is what makes a pull → edit → push round trip work for
items whose in-world names contain characters that are illegal in filenames.

Content preparation

  • Scripts are preprocessed through the normal save path, producing flattened output
    and a line map. A preprocessor error fails that file and is reported; it does not
    abort the run.
  • Notecards are sent verbatim, with no preprocessing.
  • Content is read from disk. If any selected file has unsaved editor changes, offer
    to save all before continuing.

Confirmation

Shown before any mutation. Because the target is derived from a highlight in another
view that may be scrolled out of sight, this dialog is the only place the user learns
where their files are going. It leads with the destination:

  • Object name and region.
  • Target prim — name and link number, or "root prim".
  • Then the file list, each with its disposition: create or update, and its
    resolved in-world name.
  • Counts of anything excluded: nested directories ignored, non-text files skipped.

No content comparison is performed, so the dialog cannot say which updates would
actually change anything. See Known Limitations.

Execution order

Inside vscode.window.withProgress, cancellable: true:

  1. All updates first. They are a single round trip each, fast and reliable.
  2. Then all creates. Serial, one at a time: object.item.create, then
    object.content.save against the returned item_id.

Front-loading updates means a cancellation or a timeout leaves the most value
delivered. Cancellation takes effect between items; an in-flight create or save is
allowed to complete so the reported state matches reality.

Linking

After a successful save, link the source file to the in-world item using the
linkSlItemToMaster seam introduced by pull Phase 2. The mapping is exact — push knows
which file produced which item_id — so no matching is involved.

No-modify items cannot be pushed to at all and are rejected during validation, so the
no-modify skip that applies to pull does not arise here.

Diagnostics

object.content.save returns compiled and diagnostics inline. Each pushed script
gets its own diagnostic collection entry, keyed by the source file, mapped back through
the preprocessor line map. Bulk pushes must not clobber one another's diagnostics, and
a successful recompile must clear the previous entry for that file.

Summary

One notification, with detail in the plugin log:

Category Meaning
updated Existing item overwritten.
created New item created and content saved.
created, content save failed Item exists in-world but is empty. Requires user attention.
compile failed Content saved, compilation failed; diagnostics reported.
preprocessor error Never sent; see Problems panel.
skipped (not text) File did not decode as UTF-8.
skipped (nested directory) Directory below the expanded level.
failed Create or save error; see log.

created, content save failed deserves explicit prominence — it leaves an empty script
in the user's object, and the user needs to know which one.

Validation and error handling

Checked before any mutation; all abort cleanly with a message:

Condition Behaviour
No files in the selection Abort with guidance to select files in the Explorer.
No resolvable target Fall back to object and prim quick picks; abort if still unresolved.
Object not published Abort.
No modify permission on the target prim Abort.
Duplicate target names Abort, listing the collisions.
Not connected to a viewer Abort.

During execution:

Condition Behaviour
Create timeout (30s) Count as failed, continue with the next item.
Save timeout (60s) Count as created, content save failed or failed, continue.
Object unpublished mid-run Stop, report a partial result.
Cancellation Stop after the current item; report a partial result.

Configuration

None. Type and VM come from the shared extension mapping, the target comes from the SL
explorer focus, and there is no mode to configure.


Known Limitations

Accepted for MVP and documented so they are not rediscovered as defects.

  • Silent overwrite. An item edited in-world is overwritten with no warning and no
    undo. The pre-flight difference check that would mitigate this is deferred.
  • Lossy names. An in-world item named foo/bar pulls to foo_bar.lsl. Sanitised-
    domain matching recovers it, but collision suffixes (_2, _3) are assigned in
    traversal order and could in principle transpose two items whose sanitised names
    collide.
  • Notecards named with a script extension. A notecard genuinely named config.lsl
    pulls to config.lsl and pushes back as a script named config. Type is derived
    from the extension and the extension is part of the notecard's name, so this cannot
    be fixed without recorded type information.
  • Invisible target. The destination is a highlight in another view. VS Code menu
    titles are static and cannot name it, so the confirmation dialog is the only
    safeguard.
  • New scripts run immediately. During a multi-minute push, freshly created scripts
    execute against siblings that do not exist yet.
  • Serial and slow. Create plus save plus compile per new script, one at a time.
  • No rollback. A failure midway leaves the object partially updated.

Implementation Phases

Each phase is independently reviewable.

Phase 0 — Prerequisites

  1. Export typeAndVmFromExtension from
    src/vscode/objectcontentprovider.ts and
    change the .lsl mapping from lsl2 to mono. This is a deliberate shared change
    and also alters the explorer's "New file…" behaviour.
  2. Confirm the sanitisation helper from pull Phase 0 is available; if push lands first,
    that work moves here.
  3. Confirm the linkSlItemToMaster seam from pull Phase 2 is available; if push lands
    first, that extraction moves here.

Phase 1 — Target tracking

  1. Post the focused row's identity from setFocus() in
    src/webview/explorer/explorer.ts.
  2. Record the resolved target in
    src/vscode/objectexplorerwebview.ts and
    maintain the slVscodeEdit:objectSelected context key, including clearing it on
    disconnect, unpublish and refresh.
  3. Implement focus → prim resolution per the table above, with the quick-pick fallback.

Phase 2 — Set construction and validation

  1. Register slVscodeEdit.pushFilesToObject and contribute the menus in
    package.json, including the commandPalette: false entry.
  2. Implement selection handling, one-level folder expansion, UTF-8 and size guards.
  3. Implement type, VM and target-name derivation.
  4. Implement the duplicate-name collision check that aborts the whole push.
  5. Implement pre-flight validation: published, connected, modify permission.

Phase 3 — Confirmation

  1. Build the confirmation dialog: destination first, then dispositions, then exclusion
    counts.
  2. Offer "save all" when any selected file has unsaved changes.

Phase 4 — Execution

  1. Implement the update pass: preprocess scripts, object.content.save with vm and
    running omitted.
  2. Implement the create pass: object.item.create with the derived type and vm,
    then object.content.save against the returned item_id, strictly serialized.
  3. Implement progress reporting and cancellation between items.
  4. Distinguish created, content save failed from failed in the result model.

Phase 5 — Results

  1. Link every successfully written item via linkSlItemToMaster.
  2. Route compile diagnostics per source file through the line map, without clobbering
    across files.
  3. Implement the summary notification and log detail.

Phase 6 — Surfaces and docs

  1. Update features-overview.md and
    USER_GUIDE.md.

Testing

Unit

  • Target-name derivation: scripts lose the synthetic extension, notecards keep theirs.
  • Type and VM mapping, including the .lslmono change.
  • Collision detection across explicit selections and folder expansion.
  • Sanitised-domain matching against inventory names containing illegal characters.

Integration, with a stubbed viewer client and object service

  • Mixed selection of updates and creates, verifying updates run first.
  • Folder expansion takes one level only; nested directories are counted, not pushed.
  • Duplicate target names abort with nothing written.
  • Non-UTF-8 file is skipped, not sent.
  • vm omitted on save, present on create.
  • running never sent.
  • Create succeeds, content save fails → reported as created, content save failed.
  • Cancellation between items yields a partial result matching what was actually sent.
  • Preprocessor error on one file does not abort the run.

Manual, against a viewer

  • Push to a linked prim selected in the SL explorer.
  • Push over a running script; it stays running.
  • Compile error in a pushed script maps back to the correct source line.
  • Pushed files are linked and subsequent saves reach the right item.
  • No modify permission on the target prim aborts before any mutation.

Future Work

Out of scope here, recorded so the reasoning is not lost:

  • Pre-flight difference check — fetch matching items and report which updates would
    actually change content, with a diff link.
  • Run-state control — leave new scripts stopped until the push completes, then
    start them, avoiding partial-deploy chaos.
  • A palette- and keybinding-friendly single-file command operating on the active
    editor, sharing this pipeline. Hidden commands are undiscoverable and unbindable;
    the single-file case is likely the most common one.
  • Multi-prim push, once prim identity can be recorded rather than inferred.
  • Manifest-driven deployment — a persisted definition recording target prim, item
    type, VM, run state and prune policy per file, enabling repeatable deploys and fixing
    the lossy-name and notecard-extension limitations.
  • Prune — deleting in-world items with no local counterpart, opt-in and behind an
    itemised confirmation.
  • Explicit re-link, shared with pull: bind a chosen workspace file to a specific
    in-world item.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions