aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/ui-studio.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/ui-studio.js')
-rw-r--r--modules/ext/ui-studio.js147
1 files changed, 147 insertions, 0 deletions
diff --git a/modules/ext/ui-studio.js b/modules/ext/ui-studio.js
new file mode 100644
index 0000000..e8341f3
--- /dev/null
+++ b/modules/ext/ui-studio.js
@@ -0,0 +1,147 @@
+// UI mode: studio (level 3). Chat + artifact pane fed by the real VFS.
+class UIStudioMode {
+ constructor() { this.commands = {}; this.terminal = null; }
+ requirements() { return { met: true, missing: [] }; }
+ getHelpInfo() { return {}; }
+ registerCommands() {}
+ init(terminal) {
+ this.terminal = terminal;
+ if (!terminal.uiShell) return;
+ terminal.uiShell.registerMode({
+ id: 'studio', label: 'Studio', level: 3, icon: '🎨',
+ tagline: 'Chat plus a place for your files.',
+ mount: (api) => this.mount(api)
+ });
+ }
+ mount(api) {
+ api.injectCSS('studio', `
+body[data-ui-mode="studio"] #ui-root { display: grid; grid-template-columns: 1.15fr .85fr; min-height: 0; }
+#st-left { display: flex; flex-direction: column; min-width: 0; min-height: 0;
+ border-right: 1px solid var(--border-color); }
+#st-left-bar { flex: 0 0 auto; padding: 8px 10px; border-bottom: 1px solid var(--border-color);
+ display: flex; gap: 6px; align-items: center; background: var(--bg-secondary); }
+#st-left-bar .title { flex: 1 1 auto; font-weight: 600; font-size: .95em; }
+#st-chat { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 10px; }
+#st-composer { flex: 0 0 auto; padding: 8px; border-top: 1px solid var(--border-color); }
+#st-composer #input-area { display: flex; gap: 6px; align-items: flex-end; }
+#st-composer .prompt-label, #st-composer #autocomplete-dropdown { display: none; }
+#st-composer #command-input { width: 100%; font-family: inherit; font-size: .95em; }
+#st-right { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
+#st-tabs { flex: 0 0 auto; display: flex; gap: 2px; padding: 6px 8px 0; background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-color); overflow-x: auto; }
+#st-tabs button { min-height: 40px; padding: 6px 12px; background: transparent; border: 1px solid transparent;
+ border-bottom: 0; color: var(--text-muted); font: inherit; font-size: .85em; cursor: pointer; border-radius: 8px 8px 0 0; }
+#st-tabs button.active { background: var(--bg-tertiary); color: var(--text-color); border-color: var(--border-color); }
+#st-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 10px; }
+#st-body ul { list-style: none; margin: 0; padding: 0; }
+#st-body li { padding: 9px 10px; border: 1px solid var(--border-color); border-radius: 8px;
+ margin-bottom: 6px; cursor: pointer; display: flex; gap: 8px; align-items: center; font-size: .9em; }
+#st-body li:active { background: var(--bg-tertiary); }
+#st-body .dir { color: var(--info-color); }
+#st-body .size { margin-left: auto; color: var(--text-muted); font-size: .8em; }
+#st-preview { white-space: pre-wrap; word-break: break-word; font-size: .85em; line-height: 1.5; }
+@media (max-width: 820px) {
+ body[data-ui-mode="studio"] #ui-root { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
+ #st-left { border-right: 0; }
+ #st-right { border-top: 1px solid var(--border-color); max-height: 42vh; }
+}
+`);
+
+ const root = api.root;
+ root.innerHTML = '';
+ const left = api.el('div', { id: 'st-left' });
+ const leftBar = api.el('div', { id: 'st-left-bar' });
+ leftBar.appendChild(api.el('span', { class: 'title', text: 'Chat' }));
+ const styleBtn = api.el('button', { class: 'wx11-btn', text: '⇄' });
+ styleBtn.addEventListener('click', () => api.exec('ui picker'));
+ leftBar.appendChild(styleBtn);
+ const chat = api.el('div', { id: 'st-chat' });
+ const composer = api.el('div', { id: 'st-composer' });
+ left.appendChild(leftBar); left.appendChild(chat); left.appendChild(composer);
+
+ const right = api.el('div', { id: 'st-right' });
+ const tabs = api.el('div', { id: 'st-tabs' });
+ const bodyEl = api.el('div', { id: 'st-body' });
+ right.appendChild(tabs); right.appendChild(bodyEl);
+
+ root.appendChild(left); root.appendChild(right);
+ api.attach(chat, api.stock.output);
+ api.attach(composer, api.stock.input);
+
+ const state = { tab: 'files', path: '/' };
+
+ const renderFiles = () => {
+ bodyEl.innerHTML = '';
+ const ul = api.el('ul');
+ let entries = [];
+ try { entries = api.fs.readDir(state.path) || []; } catch (e) { entries = []; }
+ // MEASURED SHAPE (virtual-fs.js:771-784): [{ name, type, size, _virtual }].
+ // type is 'dir' | 'file' | '?'; /sys/ is injected into the ROOT listing only.
+ if (!Array.isArray(entries)) entries = [];
+ if (state.path !== '/') {
+ const up = api.el('li', { text: '⬅ ..' });
+ up.addEventListener('click', () => {
+ state.path = state.path.replace(/\/[^/]+$/, '') || '/';
+ renderFiles();
+ });
+ ul.appendChild(up);
+ }
+ entries.slice(0, 200).forEach(entry => {
+ const name = String(entry.name);
+ const isDir = entry.type === 'dir' || entry._virtual;
+ const full = (state.path === '/' ? '' : state.path) + '/' + name;
+ const li = api.el('li');
+ li.appendChild(api.el('span', { class: isDir ? 'dir' : '',
+ text: (isDir ? '📁 ' : '📄 ') + name }));
+ if (!isDir && entry.size) li.appendChild(api.el('span', { class: 'size', text: entry.size + 'B' }));
+ li.addEventListener('click', () => {
+ if (isDir) { state.path = full; renderFiles(); }
+ else { setTab('preview', full); }
+ });
+ ul.appendChild(li);
+ });
+ if (!entries.length) bodyEl.appendChild(api.el('div', { class: 'wx11-dim', text: 'Empty folder.' }));
+ bodyEl.appendChild(ul);
+ };
+
+ const setTab = (which, filePath) => {
+ state.tab = which;
+ Array.prototype.forEach.call(tabs.children, b => {
+ b.classList.toggle('active', b.dataset.tab === which);
+ });
+ if (which === 'files') return renderFiles();
+ if (which === 'preview') {
+ bodyEl.innerHTML = '';
+ const pre = api.el('pre', { id: 'st-preview' });
+ try {
+ pre.textContent = api.fs.readFile(filePath || '/README.md') || '(empty file)';
+ } catch (e) { pre.textContent = 'Cannot read ' + (filePath || '/README.md') + ': ' + e.message; }
+ bodyEl.appendChild(pre);
+ return;
+ }
+ if (which === 'editor') {
+ bodyEl.innerHTML = '';
+ const target = filePath || '/notes.md';
+ const btn = api.el('button', { class: 'wx11-btn', text: '📝 Open the editor for ' + target });
+ btn.addEventListener('click', () => {
+ // Quoted: a filename with a space would arrive as two arguments (found by review).
+ api.exec("edit '" + String(target).replace(/'/g, "'\\''") + "'");
+ api.toast('Editor takes over the chat pane — close it to return');
+ });
+ bodyEl.appendChild(btn);
+ bodyEl.appendChild(api.el('div', { class: 'wx11-dim', style: 'margin-top:8px',
+ text: 'The editor module takes over the terminal pane; close it to come back.' }));
+ }
+ };
+
+ [['files', '📁 Files'], ['preview', '👁 Preview'], ['editor', '📝 Editor']].forEach(([id, label]) => {
+ const b = api.el('button', { text: label });
+ b.dataset.tab = id;
+ b.addEventListener('click', () => setTab(id));
+ tabs.appendChild(b);
+ });
+ setTab('files');
+
+ return () => { /* buttons die with the root */ };
+ }
+}