← Commit history

Skip repeated Codex IDE introductory walkthrough in new editor tabs

John Lauer ·18eb77182a ·20d ago ·parent dafeebe
7 files changed +138−5
README.md+5
@@ -106,3 +106,8 @@ release/update manifest has not been published. Installing this wiki package enables container-local Codex reload continuity, fixes startup timeouts in restored hidden tabs, and removes the typing-box fade. See [installation, extension updates, and uninstall behavior](docs/RELOAD-INTEGRATION.md).+++### Skip the repeated IDE walkthrough++Version 0.1.23 opens new Codex editor tabs directly at the prompt. The default ships through Hydrogen’s existing Codex package dependency; it preserves sign-in and permission prompts. See [diagnosis and verification](docs/CODEX-WALKTHROUGH.md). Already-open walkthrough tabs should be closed and reopened.
docs/CODEX-WALKTHROUGH.mdadded+24
@@ -0,0 +1,24 @@+# New editor tabs open directly to the prompt++Adom Codex 0.1.23 skips the known introductory IDE tour by default. Standalone editor tabs and Ctrl+Tab navigation remain available. Authentication and tool approval flows are unchanged.++## Reproduction and diagnosis++Verified 2026-09-16 with OpenAI extension 26.908.40401-linux-x64, adom-vscode 1.1.90, and Hydrogen build 459a907067baf989596bd677f8f29390709c67fa-dirty-1789512781 on the Windows/WSL2 workspace.++Adom opens `openai-codex://route/extension/panel/new?adom=<nonce>` using `chatgpt.conversationEditor`. A fresh tab displayed "Codex in your IDE", Back and Next, with no editable prompt. The extension host returned `true` for `get-global-state` key `viewed2025-09-15-nux` even while the tour was visible. Setting a completion flag alone therefore does not resolve the observed behavior. A startup/query race is suspected, not proven.++The installed extension exposes no skip-tour VS Code setting. Its NewThreadPanelPage passes through a tour guard; the tour-status hook can return a first-run route based on the completion query and authentication method.++## Package repair++`runtime/walkthrough.py` recognizes only the 2025-09-15 introductory tour-status hook and inserts an early `none` return. It does not write auth, consent or approval state. The existing package reconciler reapplies the change to recognized extension updates. Unknown or ambiguous upstream hooks are left intact and reported as unsupported. Uninstall removes only the inserted fragment, preserving unrelated edits.++This is an Adom compatibility patch, not an upstream Codex preference. The existing Hydrogen platform bootstrap 0.5.58 depends on `adom/codex ^0.1.22`, which accepts 0.1.23. New setup and package updates can therefore receive this default through the existing dependency chain. Other users are not claimed updated until their package updater has installed it.++## Verification++- 19 Python tests passed, including patch idempotency, missing/false/true completion state, unknown/ambiguous upstream refusal, and uninstall preservation.+- JavaScript module syntax check and renderer lifecycle test passed.+- A new tab opened through the live `/ai/new {type: codex, surface: tab}` API rendered one contenteditable prompt and no introductory tour after the patch. The same API rendered the tour and zero inputs before the patch.+- Existing already-loaded webviews retain their old JavaScript until reopened; active conversations need no reload.
package.json+1−1
@@ -4,7 +4,7 @@   "type": "app",   "title": "Codex",   "brief": "Codex in Adom Hydrogen: ecosystem setup, dock dashboard and live engineering demos.",-  "version": "0.1.22",+  "version": "0.1.23",   "description": "Codex in Adom Hydrogen with reusable CAD routing, electrical review, copper-ablation and narrated video skills, plus verified KiCad and Fusion demo progressions.",   "org": "adom",   "needs_sudo": false,
page.json+2−2
@@ -4,7 +4,7 @@   "type": "app",   "title": "Codex",   "brief": "Codex in Adom Hydrogen: ecosystem setup, dock dashboard and live engineering demos.",-  "version": "0.1.22",+  "version": "0.1.23",   "hero": {     "type": "image",     "path": "screenshots/hero.png"@@ -95,4 +95,4 @@     "videos/04-public-adom-solenoid-full-routing-narrated.mp4",     "videos/04-public-adom-solenoid-full-routing.mp4"   ]-}\ No newline at end of file+}
runtime/manage.py+6−2
@@ -13,6 +13,7 @@ import sys import time  from settings import change, entries+from walkthrough import patch_tour, restore_tour  ROOT = Path(__file__).resolve().parent KEY = 'chatgpt.cliExecutable'@@ -93,6 +94,7 @@ class Integration:         return result      def patch(self, extension, package):+        tour = patch_tour(extension, atomic)         main = (extension / package.get('main', 'out/extension.js')).resolve()         if not main.is_relative_to(extension.resolve()): raise ValueError('Extension entry point escapes directory')         text = main.read_text()@@ -102,7 +104,7 @@ class Integration:         if JS_BEGIN not in text:             if len(matches) != 1:                 return {'version': package['version'], 'state': 'unsupported',-                        'reason': 'Renderer startup changed upstream; extension left intact'}+                        'reason': 'Renderer startup changed upstream; renderer left intact', 'introductory_tour': tour}             m = matches[0]             # Get role and disposal callback from initializeWebview's arguments.             declarations = list(re.finditer(r'async initializeWebview\(([\w$]+),([\w$]+),([\w$]+),[\w$]+\)', text[:m.start()]))@@ -127,7 +129,7 @@ class Integration:                 if path.read_text() != original: raise ValueError('Stylesheet changed during update')                 atomic(path, original + '\n' + css)             styles.append(path.name)-        return {'version': package['version'], 'state': 'patched', 'composer_styles': styles}+        return {'version': package['version'], 'state': 'patched', 'composer_styles': styles, 'introductory_tour': tour}      def cron(self, install):         if not shutil.which('crontab'):@@ -175,6 +177,8 @@ class Integration:             fcntl.flock(mutation, fcntl.LOCK_EX)             self.marker.unlink()             if not isolated: self.cron(False)+            for extension in self.extensions.glob('openai.chatgpt-*'):+                restore_tour(extension, atomic)             for record in self.extensions.glob('openai.chatgpt-*/.adom-codex-patch.json'):                 data = json.loads(record.read_text())                 main = (record.parent / data['main']).resolve()
runtime/walkthrough.pyadded+47
@@ -0,0 +1,47 @@+"""Skip the known introductory IDE tour, not authentication or approvals.++The upstream NUX hook can redirect a new editor tab before its persisted+completion query settles. Adom opens directly at the composer by default.+Only the identified 2025-09-15 tour hook is supported; fail closed on drift.+"""+import re++MARKER = '/* adom/codex introductory tour:begin */return`none`;/* adom/codex introductory tour:end */'+HOOK = re.compile(+    r'function [\w$]+\(\)\{(?P<marker>' + re.escape(MARKER) + r')?'+    r'let\{data:(?P<data>[\w$]+),isLoading:(?P<loading>[\w$]+)\}='+    r'[\w$]+\([\w$]+\.NUX_2025_09_15\),\{authMethod:[\w$]+\}=[\w$]+\(\);'+    r'if\(!(?P=loading)\)\{if\((?P=data)\)return`none`;switch\('+)+++def patch_tour(extension, write):+    matches = []+    for path in (extension / 'webview/assets').glob('*.js'):+        # Large locale files cannot contain the hook, and reading every locale+        # on each reconciliation is needlessly expensive.+        if not path.name.startswith(('app-initial-', 'index-')):+            continue+        text = path.read_text()+        for match in HOOK.finditer(text):+            tail = text[match.end():match.end() + 800]+            if '`2025-09-15-full-chatgpt-auth`' in tail and '`2025-09-15-apikey-auth`' in tail:+                matches.append((path, text, match))+    if len(matches) != 1:+        return {'state': 'unsupported', 'reason': 'Introductory tour hook changed upstream; left intact'}+    path, text, match = matches[0]+    if not match['marker']:+        offset = text.index('{', match.start()) + 1+        if path.read_text() != text:+            raise ValueError('Webview bundle changed during update')+        write(path, text[:offset] + MARKER + text[offset:])+    return {'state': 'skipped', 'asset': path.name}+++def restore_tour(extension, write):+    for path in (extension / 'webview/assets').glob('*.js'):+        if not path.name.startswith(('app-initial-', 'index-')):+            continue+        text = path.read_text()+        if MARKER in text:+            write(path, text.replace(MARKER, ''))
tests/test_walkthrough.pyadded+53
@@ -0,0 +1,53 @@+import subprocess+import sys+import tempfile+import unittest+from pathlib import Path++sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'runtime'))+from manage import atomic+from walkthrough import MARKER, patch_tour, restore_tour++SOURCE = '''function tour(){let{data:e,isLoading:t}=query(keys.NUX_2025_09_15),{authMethod:n}=auth();if(!t){if(e)return`none`;switch(n){case`chatgpt`:return`2025-09-15-full-chatgpt-auth`;case`apikey`:return`2025-09-15-apikey-auth`;case null:return`none`}}}'''+++class Walkthrough(unittest.TestCase):+    def test_new_tab_skips_tour_and_uninstall_restores_original(self):+        with tempfile.TemporaryDirectory() as d:+            ext = Path(d)+            asset = ext / 'webview/assets/app-initial-test.js'+            asset.parent.mkdir(parents=True)+            asset.write_text(SOURCE + '\nfunction signIn(){return "sign in required"}')+            self.assertEqual(patch_tour(ext, atomic)['state'], 'skipped')+            first = asset.read_text()+            patch_tour(ext, atomic)+            self.assertEqual(first, asset.read_text())+            # No saved state, a saved false, and a completed tour all get the+            # composer; authentication remains a separate, unchanged function.+            js = first + '''+const assert=require('node:assert/strict');+const keys={NUX_2025_09_15:'tour'};+let result;+function query(){return result};function auth(){return {authMethod:'chatgpt'}};+for(result of [{data:undefined,isLoading:false},{data:false,isLoading:false},{data:true,isLoading:false},{isLoading:true}]) assert.equal(tour(),'none');+assert.equal(signIn(),'sign in required');+'''+            subprocess.run(['node', '-e', js], check=True)+            asset.write_text(first + '\n// unrelated edit')+            restore_tour(ext, atomic)+            self.assertNotIn(MARKER, asset.read_text())+            self.assertEqual(asset.read_text(), SOURCE + '\nfunction signIn(){return "sign in required"}\n// unrelated edit')++    def test_unknown_or_ambiguous_upstream_left_untouched(self):+        for source in [SOURCE.replace('NUX_2025_09_15', 'NUX_FUTURE'), SOURCE + SOURCE]:+            with self.subTest(source=source), tempfile.TemporaryDirectory() as d:+                ext = Path(d)+                asset = ext / 'webview/assets/app-initial-test.js'+                asset.parent.mkdir(parents=True)+                asset.write_text(source)+                self.assertEqual(patch_tour(ext, atomic)['state'], 'unsupported')+                self.assertEqual(asset.read_text(), source)+++if __name__ == '__main__':+    unittest.main()