A lite UI to use Bob shell!
Introduction
As developers and tech professionals, we are incredibly spoiled by the modern Software Development Life Cycle (SDLC). Tools like IBM Bob have fundamentally changed how we work. Whether we are deep in our IDEs or navigating the terminal, having Bob as an AI co-pilot makes writing code, debugging, and system architecture feel like cheating.
But a funny thing happened recently.
My colleagues on the sales and business teams started noticing what Bob could do. They saw the power of automated file analysis, seamless document extraction, and instant business planning. Naturally, they wanted in.
The catch? I couldn’t exactly hand them a command-line interface or expect them to navigate an integrated development environment. Business planners and sales executives shouldn’t have to use a Bob IDE or Bob Shell just to run a document extraction query. They needed a simple, frictionless gateway.
So, I decided to build one: a lightweight, clean web application designed solely to expose Bob’s “Plan” and “Ask” features directly in a standard browser. And the best part? I had Bob build the entire application for me. Here is how the application works and how anyone on the team can use it.
The Web App at a Glance
The web implementation of the AskBob-LightSimulator is engineered specifically to eliminate the friction of technical interfaces (like IDE integrations or Terminal-based shells) for non-technical users. It accomplishes this by serving a responsive, client-side browser interface powered by a lightweight Node.js background router.
The web interface is designed to be completely distraction-free. No complex configurations, no terminal commands. Just two dedicated tabs representing Bob’s core business-facing powers:
- Ask: A direct line to Bob’s cognitive engine for rapid file analysis, Q&A, and document breakdown.
- Plan: A structured environment to generate business planners, step-by-step strategy maps, and project outlines.
Web Architecture Overview
Key Design Pillars of the Web Architecture
-
Decoupled Process Execution: Instead of packaging the application into a heavy, conflict-prone Electron wrapper (which can suffer from shell environment variable overrides like
ELECTRON_RUN_AS_NODE), the system runs as a clean, local Node.js web server. - Real-time Server-Sent Events (SSE): To avoid the lag of waiting for a full CLI execution to complete, the server streams standard output directly back to the browser in real time.
- Adaptive Workspace Targeting: The web interface permits non-technical stakeholders to swap directory contexts dynamically, altering where Plan-mode outputs are written.
Server-Side Route Implementation & Stream Spawning
The central brain of the web engine is server.js. It parses requests, constructs a sanitized shell environment to execute the bob CLI process, and continuously flushes stdout buffers as SSE chunks.
- Code Excerpt: Dynamic Process Spawning & SSE Stream (
server.js): Here is how the server handles the incoming chat request, isolates the process from Electron-related environmental pollution, and streams back chunk-by-chunk payload updates.
// From server.js
const { spawn } = require('child_process');
function handleChat(req, res) {
// Set headers for Server-Sent Events (SSE) streaming
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
const { message, mode, workspace } = JSON.parse(body);
// 1. Sanitize & build a clean shell environment
const cleanEnv = { ...process.env };
// Electron-related variables can cause the 'bob' node execution to crash
delete cleanEnv.ELECTRON_RUN_AS_NODE;
// 2. Select CLI flags depending on the desired operation mode
const modeFlag = mode === 'plan' ? '--chat-mode=code' : '--chat-mode=ask';
const workspaceDir = workspace || process.env.BOB_WORKSPACE_DIR || __dirname;
// 3. Spawn Bob as an active subprocess
const bobProcess = spawn(
BOB_CLI_PATH,
[modeFlag, '-p', message],
{
cwd: workspaceDir,
env: cleanEnv,
shell: '/bin/zsh' // Target the system default shell safely
}
);
// 4. Listen to standard output stream & emit events to the browser client
bobProcess.stdout.on('data', (data) => {
const textChunk = data.toString();
// Render Markdown formatting & code blocks on-the-fly before shipping
const htmlChunk = renderMarkdown(textChunk);
res.write(`data: ${JSON.stringify({ chunk: htmlChunk })}\n\n`);
});
bobProcess.on('close', (code) => {
res.write(`data: [DONE]\n\n`);
res.end();
});
});
}
Client-Side Interface & SSE Parser
On the browser side, renderer/app.js issues a standard fetch call but reads the response body progressively using a ReadableStream reader. This keeps the rendering snappy and ensures that standard text outputs and markdown code blocks are styled immediately as they stream in.
- Code Excerpt: Client-Side Chunk Listener (
renderer/app.js)
async function sendMessageToBob(promptText, activeMode) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: promptText,
mode: activeMode,
workspace: currentWorkspacePath
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedMarkdown = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunkStr = decoder.decode(value);
const lines = chunkStr.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = line.slice(6).trim();
if (payload === '[DONE]') {
finalizeChatUI();
break;
}
try {
const { chunk } = JSON.parse(payload);
accumulatedMarkdown += chunk;
updateChatBubble(accumulatedMarkdown);
} catch (e) {
// Keep parsing line by line in case of fragments
}
}
}
}
}
The Prompt Assistant: Simplifying File Access
To provide a contextual awareness and simplify file access and other precisions, commands and options helper is provided from the UI;
- Relative paths (
./workspace/input/brief.docx) - Absolute paths (
/Users/admin/documents/...) - System mount points
How the File Access Helper Works
When users click the “📁 Reference File” helper button or use one of our pre-built Suggestion Chips, the UI automatically parses their selected workspace and constructs the perfect prompt structure for Bob.
Instead of requiring users to manually type out complex path strings, they are presented with a visual file picker or context-aware template chips that instantly inject the exact syntax Bob needs to resolve the file location.
UI Implementation: Suggestion Chips & Prompt Templates
We implemented a series of interactive, click-to-fill chips directly below the main prompt input. This gives users immediate, visual examples of how to query files correctly.
<div class="prompt-helper-container">
<span class="helper-label">💡 Need help? Click a template to start:</span>
<div class="suggestion-chips">
<button class="chip" onclick="applyTemplate('ask', 'Analyze the file [file_name] and summarize the key action points.')">
📄 Analyze a Document
</button>
<button class="chip" onclick="applyTemplate('plan', 'Read the project specs in [file_name] and build a skeleton structure in output/')">
🏗️ Build Workspace Files
</button>
<button class="chip" onclick="applyTemplate('ask', 'Compare the data in [file_1] with [file_2] and highlight discrepancies.')">
📊 Compare Data Sheets
</button>
</div>
</div>
Client-Side Parsing Engine
When a user selects a template, the client-side controller seamlessly swaps out generic placeholders ([file_name]) with actual file selections from the active workspace context.
Code Excerpt: Template Injection Engine (renderer/app.js)
/**
* Injects quick-start templates with active workspace variables
*/
function applyTemplate(mode, templateText) {
// Switch to the correct mode tab automatically (Ask or Plan)
switchTab(mode);
const inputField = document.getElementById('user-prompt-input');
// If a workspace is active, we can pre-populate a realistic example path
const targetWorkspace = currentWorkspacePath ? 'input/your-document.pdf' : 'document.pdf';
// Cleanly replace path placeholders with easy-to-read instructions
let finalizedPrompt = templateText
.replace('[file_name]', targetWorkspace)
.replace('[file_1]', 'input/sheet_A.csv')
.replace('[file_2]', 'input/sheet_B.csv');
inputField.value = finalizedPrompt;
inputField.focus();
// Visually highlight the parameter they need to customize
const placeholderStart = finalizedPrompt.indexOf('input/');
if (placeholderStart !== -1) {
inputField.setSelectionRange(placeholderStart, finalizedPrompt.length);
}
}
💡 Why this Matters
This small UX enhancement bridges the cognitive gap between business intent and CLI execution. A sales director doesn’t need to know that Bob is executing a targeted read command under the hood; they simply select “Analyze a Document”, replace the placeholder with their file’s name, and watch Bob stream the parsed results to their browser.
- Last but not least, the commands helper is provided on the UI as well!
iOS Implementation (Work in Progress)
To take Bob’s planning capabilities on-the-go for sales leaders, a companion native iOS client is actively being developed under ios/AskBobIOS. It interfaces directly with the local Node.js backend using a SwiftUI native view and lightweight network handlers.
Mobile-Backend Integration Architecture
Key iOS Implementation Elements
-
SwiftUI Presentation Layer: Centered around a classic messenger conversational interface, allowing easy switching between the
AskandPlanexecution schemes. -
Flexible Target Endpoints: Because local simulators run on isolated host networks, the client implements a Server Connection Sheet (managed via
ServerSheet.swift) that easily binds either tohttp://localhost:3747(for same-machine simulators) or the local LAN IP address (for testing on real iPhones over Wi-Fi).
Core SwiftUI Network Client & View Model
The state of the iOS application, the active Server URL settings, and the streaming connections are managed by AppModel.swift. Below are the key architectural structures driving this mobile-to-web bridge.
- Swift Code Excerpt: App Model State and Backend Configuration (
AppModel.swift):
import Foundation
import Combine
class AppModel: ObservableObject {
@Published var serverURL: String = "http://localhost:3747"
@Published var isConnected: Bool = false
@Published var messages: [ChatMessage] = []
@Published var isThinking: Bool = false
private var cancellables = Set<AnyCancellable>()
/// Test the node web-server connection status
func verifyConnection() {
guard let url = URL(string: "\(serverURL)/api/ping") else { return }
URLSession.shared.dataTaskPublisher(for: url)
.map { response in
// Expecting a successful HTTP status code from server.js
guard let httpResponse = response.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else { return false }
return true
}
.replaceError(with: false)
.receive(on: DispatchQueue.main)
.assign(to: \.isConnected, on: self)
.store(in: &cancellables)
}
/// Apply updated server URL configurations requested from the ServerSheet View
func applyServerURL(_ newURL: String) {
let sanitizedURL = newURL.trimmingCharacters(in: .whitespacesAndNewlines)
self.serverURL = sanitizedURL
verifyConnection()
}
}
Streaming Chat Engine (Swift Implementation)
To stream characters into the SwiftUI interface, the mobile app connects to the /api/chat route. It translates HTTP chunk buffers into readable text, updating the active bubble state in real time.
- Swift Code Excerpt: Stream Listener (
ChatViewModel.swift)
import Foundation
class ChatViewModel: ObservableObject {
@Published var activeChatBubbleText: String = ""
func sendStreamingRequest(prompt: String, mode: String, serverURL: String) async {
guard let url = URL(string: "\(serverURL)/api/chat") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let payload: [String: String] = [
"message": prompt,
"mode": mode
]
request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
do {
let (bytes, response) = try await URLSession.shared.bytes(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
return
}
// Read incoming line events progressively
for try await line in bytes.lines {
if line.hasPrefix("data: ") {
let jsonString = String(line.dropFirst(6))
if jsonString.contains("[DONE]") {
break
}
if let data = jsonString.data(using: .utf8),
let parsed = try? JSONDecoder().decode(ChatChunk.self, from: data) {
DispatchQueue.main.async {
self.activeChatBubbleText += parsed.chunk
}
}
}
}
} catch {
print("Streaming connection failed: \(error)")
}
}
}
struct ChatChunk: Codable {
let chunk: String
}
Conclusion: Democratizing AI, One Interface at a Time
By wrapping the raw power of the IBM Bob SDLC into a frictionless, lightweight environment, we have successfully bridged the gap between engineering-grade capabilities and everyday business workflows. What started as an effort to save our sales and planning colleagues from terminal screens and IDE setups has materialized into a highly functional ecosystem. Crucially, the core philosophy behind this project is not to bypass, rewrite, or overwrite Bob’s robust native functionalities; rather, it is to provide a lightweight, accessible companion that acts as a helper to open up these powerful tools to a much broader public. Through this collaborative approach, we have delivered a responsive, real-time web portal powered by Server-Sent Events, integrated a dynamic Prompt Assistant to handle workspace pathing automatically, and laid the groundwork for an on-the-go SwiftUI mobile client. The ultimate irony remains our greatest success — using Bob himself to build the very tools that liberate others from the complexities of software development. As we finalize the iOS implementation, we aren’t just deploying an application; we are empowering our entire organization to extract insights, draft strategic plans, and build business documents at the speed of thought, proving that the best technology is the kind that belongs to everyone.
Thanks for reading 👍
Links
- Github repository for this code: https://github.com/aairom/AskBob-LightSimulator
- IBM Bob: https://bob.ibm.com/







Top comments (0)