From 20c527c8edcde442a60f0d542d26274b2d6f57bc Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 16 Jul 2026 23:40:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Workspace-Umbau=20Schritt=203=20?= =?UTF-8?q?=E2=80=94=20Backend-Kanaele=20+=20code/desktop-Services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verdrahtet die neuen Kanaele "dunkel" (noch ohne UI): - rvs/server.js: ALLOWED_TYPES um code_file/code_file_edit, check_desktop/ desktop_status und vnc_open/close/data/input erweitert (Base64-in-JSON-Relay wie audio_pcm, kein Binaer-Handling noetig). - brainApi.ts: Project.kind ('code'|'chat') + desktop_url; ChatScreen publiziert die Kinds in den projectFocus-Spiegel. - services/codeFile.ts: Live-Spiegel der Code-Dateien (content + Deltas) mit Ruckkanal code_file_edit fuer eigene Edits. - services/desktop.ts: Desktop-Status + VNC-RFB-Tunnel (vnc_open/close/input/ data) durch RVS, Session = Projekt-ID. tsc clean, rvs/server.js syntaktisch OK. Co-Authored-By: Claude Opus 4.8 --- android/src/screens/ChatScreen.tsx | 7 +- android/src/services/brainApi.ts | 6 ++ android/src/services/codeFile.ts | Bin 0 -> 4857 bytes android/src/services/desktop.ts | 102 +++++++++++++++++++++++++++++ rvs/server.js | 6 ++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 android/src/services/codeFile.ts create mode 100644 android/src/services/desktop.ts diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index e1208f9..bc44dc2 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -526,8 +526,13 @@ const ChatScreen: React.FC = () => { brainApi.listProjects(true) .then(list => { const map: Record = {}; - for (const p of list) map[p.id] = p.name; + const kinds: Record = {}; + for (const p of list) { + map[p.id] = p.name; + kinds[p.id] = p.kind === 'code' ? 'code' : 'chat'; + } setProjectNameById(prev => ({ ...prev, ...map })); + projectFocus.setKinds(kinds); }) .catch(() => {}); }; diff --git a/android/src/services/brainApi.ts b/android/src/services/brainApi.ts index a7445df..e26aaab 100644 --- a/android/src/services/brainApi.ts +++ b/android/src/services/brainApi.ts @@ -162,6 +162,12 @@ export interface Project { updated_at: number; last_activity_at: number; turn_count: number; + // Workspace: 'code' blendet Editor-/VNC-Kacheln ein. ARIA setzt das selbst + // via set_project_kind; fehlt/undefined = 'chat' (nur Chat-Kachel). + kind?: 'code' | 'chat'; + // Optionale absolute noVNC-URL (falls der Desktop direkt erreichbar ist, + // sonst laeuft der VNC-Stream als RFB-Bytes durch RVS). + desktop_url?: string; } export interface ProjectStatus { diff --git a/android/src/services/codeFile.ts b/android/src/services/codeFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc9ee7cd2e35acb423ce809d6094e484c349d610 GIT binary patch literal 4857 zcmb_gUvJwu62E7EidkT>mUE7>&+Vl}ugwC*wYx#m+XJpBHnc?9iX~A;Qcj%Iz&+gu zSnMaGyyCMjy(JKD};Bz0?)u z+R|IBp1xCFm%5o#UTS)C_5KZ&4e7Gcby&Z&=2ovgb933s>T==d)K;CNt1erwRii2@ zI&jnFQa73|^3uB$Gc_}Q_@;*?C-LN!#@ zBAl^v@LPGxl_OO-W;Y!Aiw4&l>huD~vOEu+?R2&#FklNsX_MX-GATBk^-90P&TDNq zWu|GRbma-!ipsY-^Y8O{tNdyP4>Y9KzSC8Ox1UW_d3_Jt=BkE;*aIw?YnAqpw>Dkl zwGZ|+_VjNmTfvRa6b4pdUhkbhz4offX`$Dqu3=@T{ab@=jh|81J2ZE?$+b`EW9K{+xPz`PDB;fR#--j-a$PpdN_*3Yu4j`;S+{&V*5~xd=K83e zQ}E`F3TtYbro4Chax&5PLRmr@TPRqD-E^ruM0lK#P``jwHO&4JV{gWzvVkc61vzga z!OO{RGC4arp-XLD32B)p{S$Jt@@zOu;t}?EjQ5mI&YqWn(*tIMWmyh*DmZC443qHJ zcwDJw*+I+kTo&v+p4zZ@rWq88Vqb&=LkzOX9sRw{`FKuIsB)({H8~tkFRWAkfs)93 zMlcyvV~-)6*mko&)+=)UpA>td1p!qgre2Sg6>%`LcdK~GZ(AMpyY3crPDvL?*y=&L z8J)kPjVW^k2^c%1>%}D~r+-4&go(A@{#5YhC*A=PjeH}lZoYlolhN0&G@b6geFRJV zGwtpM=M-I&dANpD+g964wq$SQUMFxKDKqP*B}jr)65!|b5lqus-6!AA<9pd8ZOW_j z-cl(te2JW&acGC#-aw)K-as9OvfZ2KH(TvH+wA$e3LwKLGlLoZ874|YvINz63g>Z( zq=D8O2!TM{n_$@?Wk1A-=BV0-`g2MHp~Mlav}>!vv_3c%?ER*?OWw^ zGSawKu;`V}jLrWN5_ujI`W0Amj1t|VSWIu#M!C$Et)I>@bALQlKc&i^ObI*B>^KJZ z?7XlWPYGeW1!24OE7RaiyTHhHi!I)l0HW#A@GLX4K$+=W_GB1ln1{XOnYGYwihs;y zvM8^sVLGS??Lfy$f^ZE~o34$oNKp4{3Dh=BNV?#$Z^>ORv@Ue zN;xMGB&Z&1aDcyZ!ywCjg+sVC`VQIG?gNowYhMXog|KaH*yFkd>K^%JFO+pa3Fs$qB~{d!YYNN|A+(j*)p;P*SG? ztKXahBs0h@&C+xhZNpaTL8j$gt``YvfMh`ZOU?R|B zBIJJ{V>!QJFnLa_gEz6B0EBVH9>$XW5jm(_Tv{&|+{jGY6VHqw&+7aYr!Y z$UA6!lQp-b`xp*Rol_rX`|>)cK4x|YF7Ko(JMLCs_&;PCdF{Eg*`lsd%icU9EkjF+ z4*mi?d6Hxc!HR5=gu_Q5!R_uFTokysR}B~R(Bk6B4X>lia7{y#E|)RBsz?uf`hh!S zz87)t7;X)rYb4w%JY$|4Lcb4RfX)FT0`r1DpbI)Q(*dxL2?8jx-=UB+t)MIjX*qJt zMpfuHlGzY=9Q;Xf3^p(XGI$UyU<Yc$vtgB1 zCR3GuYwA{6oh)Q>WZ!6t;lSP!A#VE7v0cW=+{1t9&_+57RB#JBa1UHZ=d*vW^b)?Z zxbKGRUxOPv-)`Wyc)@Psogo$@C*400M-lp^g+>I83KfJ!QHtGllx~PRF(M)^q)@b9 zpnnV=W41l$g+MKFMYT!dgsUU2K+GY8v=lSe)H8Av2*y46ZspeJ359;0t#(j7*3=Zq)Wj6B;`1&f@0;)KS&p153Z*<>bFr CzbWAW literal 0 HcmV?d00001 diff --git a/android/src/services/desktop.ts b/android/src/services/desktop.ts new file mode 100644 index 0000000..688cff0 --- /dev/null +++ b/android/src/services/desktop.ts @@ -0,0 +1,102 @@ +/** + * desktop — Desktop-/VNC-Anbindung fuer Code-Projekte. + * + * Zwei Aufgaben: + * 1. Verfuegbarkeit: `check_desktop` triggert die Bridge, `desktop_status` + * meldet zurueck ob eine QEMU-VNC laeuft (und ggf. eine direkte URL). + * 2. VNC-Tunnel: der noVNC-Client in der App-WebView spricht kein eigenes + * WebSocket, sondern schickt RFB-Bytes als `vnc_input` (Base64) ueber RVS; + * die Bridge oeffnet die TCP-Verbindung zu QEMU (host:5901) und streamt die + * Antwort als `vnc_data` zurueck. Base64-in-JSON wie audio_pcm. + * + * Eine Session = ein Desktop; wir nutzen die Projekt-ID als Session-Key (leer = + * 'main'). Muster wie services/rvs.ts (Singleton mit Listener-Listen). + */ + +import rvs, { RVSMessage } from './rvs'; + +export interface DesktopStatus { + available: boolean; + session: string; + /** optionale direkte noVNC-URL (falls Host direkt erreichbar) */ + url?: string; + message?: string; +} + +type StatusSub = (s: DesktopStatus) => void; +type VncDataSub = (b64: string) => void; + +const DEFAULT_VNC_PORT = 5901; +const sessionOf = (projectId: string) => projectId || 'main'; + +class DesktopService { + private status: DesktopStatus = { available: false, session: '' }; + private statusSubs: StatusSub[] = []; + private vncDataSubs: VncDataSub[] = []; + private currentSession = ''; + + constructor() { + rvs.onMessage((m) => this.onMessage(m)); + } + + private onMessage(m: RVSMessage): void { + const p = (m.payload || {}) as any; + if (m.type === 'desktop_status') { + this.status = { + available: !!p.available, + session: p.session || '', + url: typeof p.url === 'string' ? p.url : undefined, + message: p.message, + }; + const s = this.status; + this.statusSubs.forEach((cb) => cb(s)); + } else if (m.type === 'vnc_data') { + if (this.currentSession && p.session && p.session !== this.currentSession) return; + const b64 = typeof p.b64 === 'string' ? p.b64 : ''; + if (b64) this.vncDataSubs.forEach((cb) => cb(b64)); + } + } + + getStatus(): DesktopStatus { + return this.status; + } + + subscribeStatus(cb: StatusSub): () => void { + this.statusSubs.push(cb); + cb(this.status); + return () => { this.statusSubs = this.statusSubs.filter((s) => s !== cb); }; + } + + /** Bridge fragen, ob fuer dieses Projekt ein QEMU-Desktop laeuft. */ + requestCheck(projectId: string, port: number = DEFAULT_VNC_PORT): void { + rvs.send('check_desktop', { projectId: projectId || '', session: sessionOf(projectId), port }); + } + + /** VNC-Tunnel oeffnen — Bridge verbindet TCP zu QEMU. */ + openVnc(projectId: string, port: number = DEFAULT_VNC_PORT): string { + const session = sessionOf(projectId); + this.currentSession = session; + rvs.send('vnc_open', { session, port }); + return session; + } + + closeVnc(): void { + if (this.currentSession) rvs.send('vnc_close', { session: this.currentSession }); + this.currentSession = ''; + } + + /** RFB-Bytes (Base64) aus der noVNC-WebView an die Bridge weiterreichen. */ + sendInput(b64: string): void { + if (!this.currentSession) return; + rvs.send('vnc_input', { session: this.currentSession, b64 }); + } + + /** Listener fuer eingehende RFB-Bytes (Base64) — die noVNC-WebView. */ + onVncData(cb: VncDataSub): () => void { + this.vncDataSubs.push(cb); + return () => { this.vncDataSubs = this.vncDataSubs.filter((s) => s !== cb); }; + } +} + +const desktop = new DesktopService(); +export default desktop; diff --git a/rvs/server.js b/rvs/server.js index 687fa45..aa7fb40 100644 --- a/rvs/server.js +++ b/rvs/server.js @@ -66,6 +66,12 @@ const ALLOWED_TYPES = new Set([ // Qwen3 auf der Gamebox (via Bridge → RVS → llm-adapter → llama.cpp). // llm_partial ist fuer B2 (Token-Streaming) reserviert, noch ungenutzt. "llm_request", "llm_response", "llm_partial", + // Workspace-Desktop (Code-Projekte): Live-Code-Editor (CodeMirror in der App) + // spiegelt ARIAs Datei-Writes, und QEMU-VNC wird als RFB-Bytes durch RVS + // getunnelt (Base64-in-JSON wie audio_pcm — kein Binaer-Handling noetig). + "code_file", "code_file_edit", + "check_desktop", "desktop_status", + "vnc_open", "vnc_close", "vnc_data", "vnc_input", ]); // Token-Raum: token -> { clients: Set }