Skip to content

Merge with upstream#2

Open
Outcue wants to merge 773 commits into
Outcue:masterfrom
newspeaklanguage:master
Open

Merge with upstream#2
Outcue wants to merge 773 commits into
Outcue:masterfrom
newspeaklanguage:master

Conversation

@Outcue
Copy link
Copy Markdown
Owner

@Outcue Outcue commented Aug 8, 2021

No description provided.

gbracha added 30 commits April 19, 2024 11:12
… code to co-exist in a document. To this end, amplets are assigned ids by the system rather than using their definition as a key. In addition, the worksapce manager now allows user-named workspaces to be generated via its API, which makes it easier to open workspaces in a document, keeping these distinct yet also maintaining their state.
…ng of live widgets embedded in HTML via the mapping rather than via amplets. This restores that.
…at if it is open, it stays open (and indeed is recursively updated if its internals depend on the model, and retains any state it has recursively as well).
Update version number
…d by default and eliminating explicit Ordered variants. Depends on changes to psoup repo.
…apping to cache amplets so updates would work properly. The updates work, but editing the amplet in the HTML doesn't work well. This fixes that, but is expensive and breaks some code (like Ozymandias). Further work should address that.
… effect unles sthe ampletID is changed. Now the amplets update, but no computation is done if the expression has not changed. Also, it doesn't seem to break anything (the other attempt broke Ozymandias for unknown reasons).
… for embedded windows. Needs testing and improvements, as well as UI support for splitting and unifying windows.
gbracha and others added 30 commits May 13, 2026 08:58
The AI chat could read the IDE but not drive its view. Add five
side-effecting tools so the model can open what it has just looked up:

- navigate_to_class, navigate_to_namespace, navigate_to_document
- show_tests / run_tests for test configuration classes

Browsing exposes a new public navigateTo: aSubject so external callers
can drive currentWindow enterSubject: without holding a presenter.
AI_IDE_Support adds the tool definitions, a namespaceSubjectForPath:
helper, and imports ActivationMirror as a slot (factory parameters
are not in scope inside methods). run_tests mirrors respondToRunTests
using ActivationMirror invokeSuspended: so a raise routes to a
debugging thread. The system prompt gains a side-effecting-tools
subsection so the model knows to use them only when the user has
expressed intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the AI modify code in the running IDE via two tools: propose_changes
stages a changeset and returns a textual diff plus an id; apply_changeset
runs the platform's atomic installer on the staged set. The user reviews
the diff in chat and only approved changesets ever touch the system.

Supported change kinds:
  add_or_replace_method, delete_method
  add_or_replace_lazy_slot, delete_lazy_slot
  set_class_header
  add_or_replace_nested_class, delete_nested_class
  add_top_level_class
Multiple changes in one propose_changes call install atomically — meant
for mutually-dependent edits. Top-level deletion and class rename are
deliberately out of scope (different mechanism / non-atomic).

Mechanics worth noting:
  * Changes are aggregated per top-level class into one
    ClassDeclarationBuilder; nested navigation re-uses the cached top
    builder. When descending via instanceSide.nestedClasses we patch the
    nested IR's enclosingClass to the parent's instance-side
    IntermediateMixin so the compiler's scope walk can reach the top
    (the mirror system's lazy patch-up only runs on the bottom-up path
    via mirror.asBuilder).
  * primordialsoup's AtomicInstallWrapper returns mirrors but does NOT
    put new top-level classes into Root; we mirror Browsing's own
    post-install step (applyToObject reflectee → Root at: name).

Focus refactor — brain buttons no longer track focus by Subject identity,
which broke after any method replacement: atomic install forwards mixin
objects but not individual methods, MethodMirror equality fails,
MirrorGroupSubject>>updateElements creates a fresh MethodSubject, and the
old owner-equality check goes dark. Focus now consists of three
orthogonal pieces of state in AI_IDE_Support:
  * a description block, returned by the current_focus tool
  * a validator block, run on every current_focus call so a deleted or
    replaced-away entity drops focus automatically (works uniformly for
    explicit deletes and implicit ones like add_or_replace_nested_class)
  * structured identity slots (kind / classPath / side / selector), used
    by isFocusedOnX:... matchers for brain-button highlighting
Validators compare via ClassDeclarationMirror>>= (which checks the
underlying mixin), so a replaced class — whose mixin has been forwarded
— still matches; only deletions drop out.

The dead owner-based API (setFocus:, setFocus:owner:, isFocusedOwner:,
currentFocusOwner_slot) is removed; the kind-specific setXFocusOn:
helpers are the only public way to install focus. aiChatButton: now takes
an explicit ifFocused: predicate from each brain site.

Also cleaned up `slot ::= nil` to bare `slot` in AIAccess, AIChatUI, and
AI_IDE_Support — standalone mutable slots are nil-defaulted; the form
without an initializer takes no period.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… responses

Model picker
  AnthropicProvider's model slot is now mutable so the chat can switch
  models mid-session. The session's apiMessages history is provider-
  agnostic (just text / tool_use / tool_result blocks), so switching just
  routes the next turn to a different model with the full transcript as
  context. AnthropicProvider exposes a curated availableModels list and
  displayNameFor: with friendly labels for the picker.

  ChatPresenter's heading bar grows a model picker — a label of the
  current model plus a dropdown. The picker is disabled (rendered as a
  dim label) while a request is in flight, to avoid switching in the
  middle of a tool-use loop.

  AIChatSetupSubject's default modelName is now claude-sonnet-4-6,
  replacing the year-old claude-sonnet-4-20250514. The legacy id is
  retained in availableModels for back-compat with existing chats.

  Chat help text expanded to describe the picker, available models,
  history preservation, the first-turn cache miss after a switch, and
  the in-flight guard.

Defensive response handling
  completeWithToolLoop now validates that the API response carries a
  content array before mutating apiMessages. We have observed
  intermittent 200-OK responses missing content (CDN edge case /
  refusal-with-no-body / transient server glitch) — previously the
  undefined value got stored as an assistant message, which corrupted
  history and broke every subsequent send. Now it raises a clear error
  ("API response missing content. stop_reason: ...") without touching
  state, so a retry just works.

  displayMessages picks up a matching guard so any pre-existing
  corrupted-content message in a long-running chat doesn't crash the UI.

  Non-OK API responses now include statusText alongside the numeric
  status — so "Anthropic API error: 429" becomes
  "Anthropic API error: 429 Too Many Requests".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…error handling

Evaluation surface (AI_IDE_Support)
  Five new tools — evaluate / navigate_to_object / inspect_object /
  list_objects / forget_object — let the AI run expressions and refer to
  results across turns:
  - The AI workspace is the default receiver; created lazily as a real
    IDE workspace (class AIWorkspace, subclass of Workspace, visible to
    the user via the standard workspace list) by going through
    WorkspaceManager.AllWorkspacesSubject>>getWorkspace:.
  - Non-primitive results are stored in a per-session object registry
    under auto-generated ids (r1, r2, ...) or AI-supplied names. The AI
    refers to them via in_id on later evaluate calls.
  - 'workspace' and 'focus' are pre-registered ids. 'focus' resolves
    dynamically to the currently focused evaluator's receiver (see
    below) — letting the AI write evaluate("...") and have it default
    sensibly to either the workspace or whatever evaluator the user has
    indicated.

Chats on evaluator panes (Browsing)
  EvaluatorPresenter — the base class shared by workspaces, object
  inspectors, in-method evaluator panes, lazy-slot evaluators, and
  debugger activations — grows a brain button in its button row plus a
  chatSection in its definition. Clicking installs a new #evaluator
  focus kind whose identity is the evaluator's underlying mirror
  (ObjectMirror or ActivationMirror). When that focus is active, the
  AI's evaluate default routes through it via the 'focus' id, results
  are appended to the focused evaluator's results list, and
  Browsing>>updateCurrentDisplay (new) re-renders the IDE so the user
  sees them live.

  The validator handles activation lifetime: focus on a debugger frame
  invalidates automatically once ActivationMirror>>isUncontinuable
  becomes true (frame resumed or otherwise disposed), clearing the
  focus on the next current_focus check.

Workspace UI refresh (AI_IDE_Support, Browsing)
  Workspace evaluations now also append the ThreadMirror to
  aiWorkspaceSubject's evaluator results list and call
  updateCurrentDisplay, so AI-driven evaluations show in the AIWorkspace
  presenter both live and on later navigation. The same pattern applies
  to focused evaluators.

Defensive tool error handling (AIAccess)
  executeToolCall:'s exception path now wraps e messageText in a fall-
  back chain (messageText → printString → "<unprintable exception>")
  so an oddly-shaped exception (e.g. a CannotReturn raised from a stale
  stored block) can no longer silently hang the chat.

  Memory note added for future me on the underlying bug: ^ inside a
  stored block raises CannotReturn after the defining method exits;
  tool handlers must use ifTrue:ifFalse: rather than early returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ixes

Test result and re-run tools (AI_IDE_Support)
  run_tests captures the testing subject keyed by config simple name.
  Five new tools work against the cached Tester:
    get_test_results          counts plus per-failure/error one-liners
    get_test_failure_details  full description / exception detail
    run_failing_tests         re-run current failures
    run_erroring_tests        re-run current errors
    run_test                  re-run a single named test
  Test cases are identified by "ContextName >> test_selector".
  Selective re-runs iterate a copy of the failure/error list (tester
  run: mutates the lists) and updateCurrentDisplay so the IDE view
  refreshes live. (rounded — not truncated — on the completion ratio;
  Float in primordialsoup doesn't define truncated.)

Chat on test outcome view (MinitestUI)
  TestingOutcomePresenter — already a ProgrammingPresenter — gains a
  brain button in its heading row and a chatSection in its column.
  A new #testRun focus kind identifies the run by config class path,
  with description "Test results: <ConfigName>". Validator uses
  classStillExists:.

  Run Again creates a brand-new TestingOutcomeSubject (different object),
  so Hopscotch's updateVisualsFromSameKind: chat-restore path doesn't
  apply — the new presenter's chatHolder starts empty. Definition now
  schedules an installAIChat when a matching #testRun focus is active,
  so the chat region persists across re-runs without the user having
  to re-click the brain. TestingInProgressPresenter is left alone (it
  extends Presenter not ProgrammingPresenter, is transient, and is
  navigated away from as soon as tests complete).

Chat input accept-button fix (AIChatUI)
  Clicking the green accept button used to silently fail — CodeMirror's
  setValue fires a 'change' event, which our changeResponse handler
  used to write the editor text back into subject draft, so by the
  time sendDraft ran, the draft was empty and the message wasn't sent.
  Cmd-Return won that race. The fix bypasses subject draft entirely:
  capture text locally, push it through a new ChatSubject>>sendText:
  that doesn't read draft. Clear editor + leaveEditState happen BEFORE
  updateGUI so the old fragment's edit-state machinery handles them
  rather than a fresh post-update fragment whose handlers don't see
  the old state (which had been causing the accept/cancel bar to stop
  appearing on subsequent edits).

Auto-close other chats on the same page (Browsing, AI_IDE_Support)
  Without coordination, clicking a different brain leaves the previous
  chat region visible — multiple chat panels at once. AI_IDE_Support
  tracks the active (chatHolder, pageSubject) pair; on brain click,
  the previous holder is cleared ONLY when the new click is on the
  same page. Cross-page navigation preserves each page's chat: A's
  chat survives A → click B → click → back to A. Page identity is
  Browsing's currentWindow currentPresenter subject, surfaced via
  Browsing>>currentPageSubject.

  Boolean and: chains one block, not two — Smalltalk-ism caught after
  the first build broke per-presenter brains with "False and:and:".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…list/delete media

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s the problem that the top line may not be visible if the line is short or overlaps with the accept/cancel buttons. Change show evey editor displays changes, so not great. Eventually, w emay want to change how the display of changes works so we get a signal distinct from the change buttons.
…egration, multi-doc switching, live-view reactivity, generic-doc brain, defensive extractTextFrom

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…key storage, defensive translation, surface API error bodies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…se/Newspeak-exception bridge bug, surface API errors as in-chat turns, add provider chooser to setup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hopscotch now owns AIChatUI as a lazy module instance constructed from a
platform-level aiAccess and an aiChatUIClass re-export, so HopscotchWebIDE
and AI_IDE_Support no longer instantiate the AI modules themselves. They
pull presenter classes and the chat module through platform hopscotch,
keeping the Hopscotch factory signature unchanged.

Also a type-annotation and comment pass across AIAccess, AIChatUI, and
AI_IDE_Support to document the provider abstraction, the chat-document
projection model, and the JS Promise/then: rejection idiom that replaced
in-closure Error signal: raises.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat UI classes (AIColors, AIChatSetupSubject, AIChatSetupPresenter,
ChatSubject, ChatStatusPresenter, ChatInputPresenter, ChatPresenter) now
live as sibling top-level classes inside HopscotchForHTML5 rather than
as a separate AIChatUI module. This removes the construction cycle that
required Hopscotch to retain a private platform reference and a lazy
aiChatUI slot, and removes the unnatural Platform.aiChatUIClass accessor.

AIAccess slot block is reshaped to do only class imports plus a
platform handle; its JS-side handles (global, JSON, Promise) become
lazy module-body slots in AIAccess itself. This lets Hopscotch's slot
block safely call `p aiAccess` during platform init, since AIAccess
construction no longer touches JS aliens that aren't yet usable at
snapshot time.

AI_IDE_Support drops its chatUI slot and pulls colours through a new
aiColors slot — a single small utility class on Hopscotch keeps the
import surface tidy.

The Hopscotch factory signature is unchanged, so Croquet and other
Hopscotch consumers don't need any edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GeminiProvider previously inherited the Provider default hasToolUse:
^false, so the Session's tool loop was bypassed entirely. It now
translates Gemini function-calling to and from the same tool-block
surface Session already drives for Anthropic.

Provider gains addToolResults:toMessages: as a new required hook for
tool-supporting providers, factored out of completeWithToolLoop's
inline tool_result-message assembly. AnthropicProvider's implementation
is the lifted inline code unchanged. GeminiProvider's wraps the round's
parts as {role: 'user', parts: [...]}.

Gemini-side translation:
- buildToolsPayload: produces [{functionDeclarations: [...]}], with each
  declaration using 'parameters' (Gemini) rather than 'input_schema'
  (Anthropic) for the JSON schema.
- hasToolUse: / toolUseBlocksFrom: scan candidates[0].content.parts for
  functionCall parts. Gate on functionCall.name being non-nil, not on
  functionCall itself — on text-only parts the missing functionCall
  field surfaces as JS undefined which fails isNil, and a fc-isNil-
  ifFalse guard incorrectly admits text parts and yields malformed
  shims.
- Gemini issues no tool-call ids of its own, so toolUseBlocksFrom:
  synthesises a 'gem-N' id per call and stashes the name in a per-round
  pendingCallNames map. The shim blocks expose the same 'id', 'name',
  'input' surface Anthropic emits, so Session.executeToolCall: needs no
  provider-specific path.
- makeToolResult: builds a functionResponse part, recovering the
  function name from pendingCallNames. Gemini has no standard is_error
  flag; failure text goes under 'error' instead of 'result'.
- doRequest: now accepts and plumbs the tools payload into the request
  body; complete: and attemptComplete: thread it through.

Bulletproof alien field reads (cross-cutting; surfaced during Gemini
bring-up but applies to both providers):

- New Provider helper safeStringAt: receiver field:. Returns the value
  only if a real String (verified by v , '' succeeding). Returns nil
  for missing fields, JS undefined, JS null, numbers, objects — any-
  thing that fails the no-op concat. Cheap, no JSON round-trip.
  Necessary because primordialsoup's alien bridge does not uniformly
  convert JS undefined to Newspeak nil; a missing field on a JS object
  often returns JS undefined which doesn't satisfy isNil, and the
  previous isNil-ifFalse guards admitted it into String concats and
  crashed with ArgumentError.
- All scalar string-leaf reads in both providers' response-walking code
  (extractTextFromResponse:, displayMessagesFromMessages:, hasToolUse:,
  toolUseBlocksFrom:) now go through safeStringAt:. safeAt: continues
  to handle structural navigation (returns nil for missing receivers /
  fields but doesn't normalise undefined leaves).

Robust error surfacing:
- New module-body errorDescription: helper extracts a non-nil String
  from any exception. Tries messageText, falls back to class name
  printString, then printString, then '<unprintable exception>'. Each
  step wrapped so no failure in the chain can sink the whole error
  path.
- The four Promise.reject sites (AnthropicProvider doRequest:,
  GeminiProvider doRequest:, Session completeWithToolLoop, webFetch:)
  now route through errorDescription: instead of bare e messageText,
  which was nil-prone and made the chat error turn read 'nil'.
- Session completeWithToolLoop's outer catch now ships the raw provider
  response in the rejection ('[stage: X] <error> | response: {...}'),
  so future protocol drift or a new provider's first unexpected
  message shape lands directly in the chat — no fresh diagnostic
  instrumentation needed when the AI vendor tweaks their format or a
  new provider follows the recipe.
- Stage tracking in completeWithToolLoop and executeToolCall: pinpoints
  which substep of response processing failed. JSON dump of the
  problematic tool-call block in executeToolCall:'s structural-error
  paths surfaces malformed shims directly.

Browsing.ns: ProgrammingPresenter>>aiChatButton:ifFocused: gained
multi-stage error reporting on click. The previous on:Error do:
formatted 'AI chat error: ', e messageText, which itself crashed when
messageText was nil — silently turning a recoverable UI failure into a
hard IDE freeze. The new handler tracks the substep being executed
(read activeChatHolder, install focus, install AI chat, etc.) and uses
a nested catch that falls back through messageText, class name, and
printString.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hat doc

Three related cleanups to the AI-chat entry points.

Hide-key toggle (HopscotchForHTML5.ns AIChatSetupPresenter):
- The API key editor now lives inside a ToggleComposer. Initial state
  follows a new keyRevealed slot: revealed (=true) on first-time setup
  when subject apiKey is empty, concealed (=false) once a key has been
  stored. The user can expand or collapse manually; the editor's
  acceptResponse: collapses the toggle on Accept and respondToStart
  collapses on successful chat start. Both push keyRevealed:: false
  via updateGUI:, but the close also calls keyToggle collapse directly
  to mutate the *old* toggle's isExpanded — ToggleComposer's update-
  VisualsFromSameKind: copies isExpanded from the old fragment, so a
  fresh toggle with initiallyExpanded:false alone wouldn't actually
  close after the first render.

Provider-switch resync (AI_IDE_Support.ns IDEChatSetupPresenter):
- Replaces the inline providerPicker action blocks with a new
  respondToProviderSwitch:defaultModel: that resyncs presenter state
  to the newly-chosen provider. Just mutating subject providerName /
  modelName / reloadApiKey wasn't enough: ToggleComposer preserves
  old isExpanded across re-renders, and CodeMirrorFragment preserves
  the old editor's text, so the visible form lagged behind the
  subject. The new method explicitly toggles keyToggle, updates
  keyEditor's textSlot, and updates modelEditor's textSlot in the
  same updateGUI: block, so the diff inherits the right state.

Toolbar → chat doc (AI_IDE_Support.ns chatLauncher):
- Toolbar AI Chat button now navigates to the active chat doc's full
  DocumentSubject view (Ampleforth contents with chatControls, doc
  switcher, model picker, structural amplets, turns as HTML
  sections) — the canonical home of the chat. Previously it landed
  on the bare IDEChatPresenter view, which lacks all of those.
  ensureSharedChat (used by per-presenter brain buttons for the
  inline ephemeral chat region) is unchanged; the two paths now
  give appropriately different UX for the same conversation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatDocument>>noteLiveViewRendered: stores the current TwoViewEditor
on the doc each time its DocumentPresenter renders, but the slot is
never cleared when the user navigates away. A chat-subject side
effect (sendText:, markComplete, markError:) firing AFTER navigation —
e.g. a tool-loop response resolving on a chat we just switched away
from — then calls refresh on the now-detached editor. That walks
into shell on a parent-less DocumentPresenter and raises
'hierarchy not installed in a shell'.

Fix: refreshLiveView probes the cached editor's liveness by asking
for shell; if walking the parent chain raises, the slot is stale, so
we drop it and return. The next render of this doc installs a fresh
editor via noteLiveViewRendered: and subsequent refreshes work
against that.

This is a narrow patch for the immediate footgun. The broader chat-
document reactivity / deactivation story still wants a proper hook
(see CHAT_SWITCHING_FOLLOWUPS_2026-05-18.md item 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings Matthias Kleine et al's MemoryHole (Pur version-control
abstraction; see HPI tech report 2012) into the primordialsoup-side
codebase. Phase 1 of the port: get the abstraction core to load
against the web platform. No backend, no UI, no actual VCS
operations yet — those come in follow-up commits.

The slot-block edits split roughly into:

- Squeak collections / streams imports swapped to web equivalents
  (Set / List / Map → p collections; OrderedCollection / Dictionary
  go away).
- Squeak-only deps that have no web counterpart yet (Stream,
  ExternalLauncher, Monitor, MultiByteBinaryOrTextStream,
  CrLfFileStream, Transcript, FileDirectory; TextDiffBuilder,
  DiffElement, Text/TextColor/TextEmphasis/RunArray; PackageInfo,
  PNGReadWriter, Script) → bound to nil. Their consumers are all in
  backend-or-rendering code paths the abstraction core doesn't
  exercise; phase 2 will provide them via concrete backends or
  reimplementation.
- VCSDiffing: Character constants (Character tab / space / cr / lf /
  newPage) aren't reachable in this module's scope on the web
  platform without an explicit import path; stubbed with literal
  whitespace and Set new. Same fate for String>>asSet which isn't
  in the web collections.
- VCSNewspeakSourceMirrors: `p mirrors grammar TypedNS3Grammar` MNUs
  on MirrorsForPrimordialSoup. The original code commented this as
  "cheating"; nilled out. Only reached via SourceMirrorParser, which
  is itself only instantiated by MirrorCache, which we lazify (see
  below).

The bigger structural change is breaking the `||` simultaneous-slot
dependency between the four sub-modules:

  - VCSLib's VCS class used `|| ... ||` so each sub-module's slot
    block could read its siblings via vcs.<other-module>. That
    machinery wants a Future class in scope that VCSLib doesn't
    have; the resulting submodule load fails with
    `MessageNotUnderstood: VCS Future`.
  - Fix: every cross-module slot-block binding moves to a
    `lazy public` slot in the module body. Each module captures vcs
    in a `vcsHolder` slot and reads vcs.<sibling>.<X> lazily on
    first method-time access — by which point every module's slot
    block has finished. The four sub-modules in VCS itself are
    similarly lazy public, so VCS instance creation no longer
    triggers any sub-module construction.

Affected lazy bindings:
  - VCSCore:       importedSourceMirrors, Differencer
  - VCSSourceMirrors: Differencer, newspeakSourceMirrors
  - VCSNewspeakSourceMirrors: SourceMirror, StringMirror,
    compilationUnitCache, methodDeclCache

Other small clean-ups:
  - Logger>>logMonitor slot dropped; the only reference was in a
    commented-out critical: section and a Monitor primitive isn't
    needed in the single-threaded browser anyway.
  - VCSLib manifest slimmed: VCSCore + VCSDiffing + VCSSourceMirrors
    + VCSNewspeakSourceMirrors only. UI, the three concrete backends
    (Git / Mercurial / Subversion, all of which shell out via
    ExternalLauncher), SmalltalkSourceMirrors, and
    VCSImageMirrorCaching are dropped from the active manifest and
    will return in later commits — most likely with a brand-new
    VCSIsomorphicGitBackendProvider replacing the Squeak-CLI git
    backend, since shelling out doesn't translate to the browser.

Test scaffolding:
  - MemoryHoleTesting: a TestContext with six smoke tests that
    instantiate VCSLib via packageUsing: ideNamespace, build a VCS
    against a minimal IDEStub, and confirm each public sub-module
    accessor returns non-nil. Forces every slot block to evaluate,
    plus the lazy cross-module bindings to fire when each
    sub-module's getter is called.
  - MemoryHoleTestingConfiguration: standard packageTestsUsing:
    factory. Builds the VCSLib instance at config-init using
    ideNamespace itself as the manifest (since the IDE namespace
    exposes top-level classes by name). IDEStub + NamespacingStub
    nested classes provide the minimal ide.namespacing.Root surface
    the slot blocks read.
  - tool/run-tests.sh: accepts extra .ns dep files after the test-
    config argument. The auto-discovery only looks for one IMPL_FILE
    matching TEST_BASE; multi-file packages like MemoryHole (six
    .ns files) need the dep list passed in explicitly.

All six tests pass via:

    ./run-tests.sh MemoryHoleTestingConfiguration \\
      VCSLib.ns VCSCore.ns VCSDiffing.ns \\
      VCSSourceMirrors.ns VCSNewspeakSourceMirrors.ns

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the MemoryHole port (see SESSION_SUMMARY_2026-05-20_MEMHOLE.md
outstanding item #2). The compilationUnitCache / methodDeclCache lazy
slots in VCSNewspeakSourceMirrors are now functional, and an end-to-end
smoke test parses a minimal toplevel class through them.

- TypedNS3Grammar sourced via `ide grammar` (HopscotchWebIDE already
  exposes a NewspeakGrammar instance), consistent with how Root is
  fetched via `ide namespacing`.
- InMemoryMirrorCache replaces the nilled imageMirrorCache; per-
  production string->mirror map, lives for the module's lifetime. No
  image-backed persistence (the Squeak-era cache is gone).
- MirrorCache no longer uses `parser perform: selector` (primordialsoup
  confines perform: to mirrors). Takes a productionFromParser: block
  instead; the selector is still kept as the cache key.
- mirrorForString: added as the stream-free entry point on both
  MirrorCache and the outer module. mirrorForStream: delegates.
- isCollection -> isKindOfCollection (3 sites).
- classDecl wrapper extended to 7 args, matching the grammar's
  typeParameterDecl opt slot. The new param is accepted and ignored
  for now (mirror layer doesn't yet model generic class params).
- SourceReference>>contents simplified to string-only; the Squeak
  stream-handling branch is dropped.

Tests: MemoryHoleTesting gains SourceMirrorParserTests with three
checks - the two lazy caches load, and testParseMinimalToplevelClass
parses `class Foo = ( ) ( ) : ( )` through mirrorForString: and
asserts the resulting ClassSourceMirror's name and category.

MemoryHoleTestingConfiguration's IDEStub extended to provide a real
`grammar` accessor (NewspeakGrammar usingPlatform: parsers:
CombinatorialParsing) built from the test namespace.

Depends on the ifNotNil: / isNil family additions to _Object in
primordialsoup (commit 5f7699e).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds VCSLib + the four phase-1 sub-modules (VCSCore, VCSDiffing,
VCSSourceMirrors, VCSNewspeakSourceMirrors) and the MemoryHole testing
modules to HopscotchWebIDE, so they're reachable from the workspace
and picked up by the test runner alongside other configurations.

- private manifest imports for the five MemoryHole modules.
- testModules collection extended with MemoryHoleTesting and
  MemoryHoleTestingConfiguration (the latter is what the IDE
  recognizes as a runnable configuration via packageTestsUsing:).
- Explicit at:put: entries in both populateNamespaceUsingPlatform:
  and namespaceGivenPlatform: for the five modules.

Archived Squeak-era pieces (Git/Hg/Svn launcher backends,
SmalltalkSourceMirrors, VCSImageMirrorCaching, VCSUI) are deliberately
not exposed; they have unported Squeak deps and aren't in VCSLib's
phase-1 manifest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First piece of the MemoryHole git backend port. Proves the JS bridge
end-to-end: a Backend can do local repo ops (init, add, commit, log)
and remote ops (clone via HTTP smart protocol through a CORS proxy),
all chained via the alien Promise bridge.

Module shape (intentionally MVP scope):
- Backend pairs the isomorphic-git facade with one lightning-fs
  instance scoped to the 'newspeak-memhole' IndexedDB store.
- smokeTest exercises mkdir -> init -> writeFile -> add -> commit ->
  log against a local dir, resolving to a String summary.
- cloneFrom: clones a remote URL into cloneDir via a CORS proxy. The
  corsProxy slot is mutable; defaults to http://localhost:9999 for
  dev. The deployed IDE will need a project-hosted proxy on the same
  origin so end-users don't run their own.
- Class-side smokeTestUsingPlatform: and
  cloneSmokeTestUsingPlatform:url:[corsProxy:] variants for one-line
  workspace invocation.

The AbstractBackend / AbstractRepository / AbstractHistorian lift is
deferred to a follow-up commit. That work has to reshape the abstract
interface from synchronous (Squeak shell-outs) to Promise-based
(isomorphic-git is fundamentally async).

Exposed in the IDE namespace via HopscotchWebIDE so it's reachable
from a workspace.

Dev-side dependency: a CORS proxy is required for any clone/fetch/push
to GitHub (GitHub's git smart-HTTP endpoints don't send CORS headers).
tool/cors-proxy.py is a stdlib-only Python proxy reproducing
@isomorphic-git/cors-proxy semantics. Run via

    python3 tool/cors-proxy.py

(default port 9999). Standalone, no Node, no Homebrew, no Docker.

Smoke-test invocations from an IDE workspace:

    (VCSIsomorphicGitBackendProvider smokeTestUsingPlatform: platform)
        then: [:summary | summary out].

    (VCSIsomorphicGitBackendProvider
        cloneSmokeTestUsingPlatform: platform
        url: 'https://github.com/octocat/Hello-World')
            then: [:summary | summary out].

Depends on the isomorphic-git / lightning-fs / GitHttp <script> tags
landing in primordialsoup/meta/shell.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tracked newspeak/primordialsoup.html was behind newspeak/out/
primordialsoup.html after the isomorphic-git + lightning-fs + http/web
<script> tags landed. The build chain regenerates out/primordialsoup.html
from primordialsoup/meta/shell.html on each rebuild, but the tracked
root copy isn't on that path and was left stale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the AI ask the user to pick a file via the OS file picker; the
file's contents come back to the model in tool_result.content as one
of three Anthropic content-block shapes:

  - image/*          -> image block, source.type 'base64'
  - application/pdf  -> document block, source.type 'base64'
  - everything else  -> text block (file is read as text)

Capability-gated. Provider gets a new supportsFileUploads predicate
(default false; AnthropicProvider returns true) and tools are now
built per-provider via toolsForProvider: rather than the flat tools
list. Providers that can't carry image/document blocks don't get the
tool offered.

Session and document plumbing for user-side attachments:

  - Provider#addUserText:documents:toMessages: builds a user content
    array with the message text plus one document block per attached
    file (each a text-source document for now; binary attachments are
    delivered by the tool, not by the user message).
  - Session#sendMessage:attachments: routes through it.
  - IDEChatDocument#sendText:attachments: and ChatSubject#sendText:
    attachments: forward attachments down the chain.

FileChooserFragment gains cancel handling. chooseFileList:ifCanceled:
and chooseSingleFile:ifCanceled: wire the <input type=file> 'cancel'
event (Chrome 113+, Firefox 91+, Safari 16.4+) so the tool can reject
its promise when the user dismisses the dialog instead of hanging.

Mechanical rename in AIAccess and AI_IDE_Support: the
`retainedPlatform = platform` slot becomes `js = platform js`, and
the call sites read `js localStorage` / `js global` directly. The
previous slot name was misleading (it implied "retained across the
snapshot freeze" but the only usage was platform.js) and the new
form is shorter at the call sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Primordialsoup's alien bridge does not pass arguments to a Newspeak
closure invoked synchronously by the JS Promise constructor — both
resolve and reject arrived as nil, so the executor pattern
`Promise new: [:resolve :reject | ...]` was unusable. Both pickFile
and readFileAsBase64: now use Promise.withResolvers() and invoke
the returned function aliens via Function.prototype.call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A sibling of Backend in VCSIsomorphicGitBackendProvider, dedicated to
recording IDE Accepts as commits. Owns its own lightning-fs instance
scoped to a named IndexedDB store so its objects don't intermingle
with other repos.

API:
  recordSnapshot: files <Map[String, String]> message: msg
    Writes each filename -> content, git-adds them, commits with the
    given message and the canned 'IDE Drafts' author. Resolves to the
    new commit SHA.
  history
    Resolves to git.log's JS array of commit objects (newest first).
  snapshotAt: sha
    Resolves to a JS array of [filename, content] pairs reconstructing
    the tree at that commit. Returns the raw pair array rather than
    a Newspeak Map: returning Newspeak collections from inside JS
    Promise then: closures doesn't round-trip cleanly through the
    alien bridge (the outer chain gets back garbage).

ensureInitialized is idempotent and cached - first access does mkdir +
git init, subsequent calls short-circuit via Promise.resolve.

Files are opaque: this class doesn't know that the contents are
Newspeak class sources. The IDE side decides the path scheme and
serializes mirrors into the (filename -> content) Map handed to
recordSnapshot:.

Class-side draftsSmokeTestUsingPlatform: records two snapshots, reads
the log, reconstructs the first snapshot; resolves to a summary
string. Uses its own 'newspeak-drafts-smoketest' store so it doesn't
interfere with any IDE-side drafts state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every successful Accept on installFromBuilders:into: now writes the
touched class sources to a hidden drafts repo and commits. Both
editor-side accepts and AI apply_changeset go through this method,
so a single hook covers both paths.

- New lazy public 'drafts' slot constructs the DraftsRepo on first
  access (typically the first Accept), backed by the
  'newspeak-ide-drafts' IndexedDB store at /drafts. Survives across
  reloads. Lazy so sessions that never edit pay no cost and so the
  isomorphic-git / lightning-fs <script> tags are guaranteed loaded
  by first use.
- New 'platformHolder = p.' slot captures the factory parameter so the
  lazy slot's initializer (which runs at first access, not at instance
  construction) can reach the platform.
- New recordAcceptForBuilders: builds a filename -> source Map keyed
  by '<ClassName>.ns' and fires drafts.recordSnapshot: with the
  message 'Accept: <names>'. Promise is intentionally not awaited;
  the Accept completes synchronously and the commit lands in the
  background. Rejections log to the console via 'out' but don't fail
  the install.
- installFromBuilders:into: calls recordAcceptForBuilders: after the
  install + the existing localStorage backup completes, so the drafts
  commit only fires for successful installs.

Inspect history from a workspace:

    ide drafts history then: [:log |
        0 to: (log at: 'length') - 1 do: [:i |
            ((log at: i) at: 'oid'), ' - ',
                (((log at: i) at: 'commit') at: 'message')) out]]

Bootstrap restore (loadFrom:usingPlatform:) bypasses
installFromBuilders: and uses atomicInstaller directly, so reloads
don't generate a spurious "Accept" commit on startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tool handlers may now resolve to a {text, media} JS object (or a plain
String for text-only, back-compat). Session normalizes both shapes,
then calls the new makeToolResult:textResult:media:isError: on the
provider. The per-call return is opaque to Session and folded into
messages by addToolResults:toMessages:.

Anthropic returns one tool_result block whose content array is a text
block followed by image/document blocks per supported media (image/*,
application/pdf). Audio/video are dropped with a notice appended to
the text so the model sees the omission rather than silent data loss.

Gemini returns an array of parts: a functionResponse part plus one
inlineData sibling per supported media (image/*, application/pdf,
audio/*, video/*). addToolResults:toMessages: flattens the per-call
arrays into a single user-role message's parts list. Filenames are
surfaced via an "Attached files: ..." line appended to the response
text, since Gemini's inlineData has no native title slot.

When a handler returns only media (text is empty), Session substitutes
"See attached" so both providers receive non-empty text.

upload_file is rewritten: fileToContentBlock: is replaced by
fileToToolResult: which builds the canonical {text, media} shape.
MIME-to-native-block dispatch moves out of the tool and into the
provider, where each provider knows its own capabilities.

GeminiProvider supportsFileUploads flipped to true. The upload_file
tool is now offered to Gemini sessions.

This is option 1 of the cross-provider media design (structured handler
return). The opaque-return contract leaves room for a future
OpenAI-compatible provider to emit a tool message plus a follow-up
user-role message carrying media (since OpenAI's tool channel doesn't
accept binary content) without further refactor of Session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants