Compare commits

..

No commits in common. "633ebf4a784f00f28e86ff0c52cdc4b23f6f89a5" and "96663f7c8854fafb44f2fb826ed9a9269f072bbe" have entirely different histories.

21 changed files with 359 additions and 1039 deletions

1
.gitignore vendored
View File

@ -3,5 +3,4 @@ gadget-drone.*.log
logfetch
.gadget
gadget/
node_modules

287
CHANGE.md
View File

@ -1,287 +0,0 @@
# CHANGELOG
This document tracks all significant changes to Gadget Code, organized by date.
---
## 2026-05-15 (Thursday)
**Context Window Configuration Added**
The most recent change introduces the `numCtx` (Context Window) setting to the SESSION panel, allowing users to configure the context window size for their AI model sessions. This setting is part of a broader enhancement to the model configuration pipeline that also includes `numPredict` and `maxCompletionTokens` parameters.
### Changes:
- Added `numCtx` setting to SESSION panel
- Updated model configuration pipeline to include `numPredict`, `numCtx`, and `maxCompletionTokens`
- Modified ChatSessionView to handle new context window settings
- Updated session initialization logic to apply context window configuration
- Added Gab AI affiliate link
- Changed Ollama abort procedure
---
## 2026-05-14 (Wednesday)
**SubProcess Observability Implementation**
This week focused on implementing comprehensive observability for SubProcess operations within the DroneManager and DroneInspector components, including detailed logging, status tracking, and event emission capabilities.
### Changes:
- Implemented Phase 2: SubProcess observability in DroneManager and DroneInspector
- Added subprocess status tracking and event emission
- Updated UI to display subprocess execution details
- Fixed mode change log message formatting
---
## 2026-05-13 (Tuesday)
**Documentation and Editor Improvements**
Documentation updates include adding continuation prompts and remaining steps for User Mode, session summary documentation for MVP completion, and formatting cleanup in AGENTS.md. The editor improvements address a critical issue with CodeMirror editor height constraints in flex layouts.
### Changes:
- Added continuation prompt and remaining steps for User Mode documentation
- Added session summary for User Mode MVP completion
- Fixed CodeMirror editor height constraint in flex layout
- Minor formatting cleanup in AGENTS.md
- Created new files and prep work for landing page project
---
## 2026-05-12 (Monday)
**FILES Panel and Editor Integration**
This week marked a significant milestone with the implementation of the FILES panel foundation featuring lazy-loading file tree capabilities. The implementation replaced react-ace with @uiw/react-codemirror to resolve React 19 compatibility issues and Vite dev server problems.
### Changes:
- Implemented FILES panel foundation with lazy-loading file tree
- Replaced react-ace with @uiw/react-codemirror
- Fixed flexbox scrolling for FILES panel sidebar
- Added shrink-0 to SESSION and PROJECT panels for proper flex layout
- Fixed file tree scrolling and text selection issues
- Fixed file tree backend recursion breaking lazy loading
- Fixed file tree showing entire workspace instead of project directory
- Fixed file tree duplication and corruption on expand
- Fixed file tree expand/collapse not rendering children
- Added navigation to project in Project Manager using slug
- Fixed ACE editor integration crash — React 19 compat, Vite ?url pattern, Error Boundary
- Fixed file tree duplication and corruption on expand
---
## 2026-05-11 (Sunday)
**Subagent Processing and Authentication Fixes**
This weekend's work focused on subagent processing improvements and authentication fixes, including fixes for subagent response handling, session data management, and adding the persona field to sign-in responses.
### Changes:
- Fixed subagent response handling and session data
- Added persona field to sign-in response
- Fixed JWT expiration bugs and drone heartbeat timeouts
- Added installation guides, config examples, and systemd service files
- Fixed subagent markdown output whitespace using .subagent-compact.gadget-markdown
- Applied CSS and style fixes for subagent display
- Updated build prompt
- Fixed auth: include persona field in sign-in response
---
## 2026-05-10 (Saturday)
**Workspace and Project Philosophy Shift**
This weekend involved a philosophical shift in how Workspaces and Projects are conceptualized and implemented, with improved separation of concerns and clearer boundaries between the two concepts.
### Changes:
- Applied philosophy shifts for Workspaces and Projects
- Updated workspace and project conceptual model
- Improved separation of concerns between workspace and project
- Chat interface fixes and cleanup
- Credential provider fix
- Ensure apiKey is presented to required consumers
---
## 2026-05-09 (Friday)
**Agentic Workflow Loop Rebuild**
This week involved a complete rebuild of the Agentic Workflow Loop to improve reliability and performance, including better error handling, improved checkpointing, and enhanced event emission capabilities.
### Changes:
- Re-built Agentic Workflow Loop with improved reliability
- Enhanced streaming response handling
- Implemented chat session auto-naming with IDE update
- Improved checkpointing and event emission
- GadgetLogTransportSocket and the drone-to-IDE log
---
## 2026-05-08 (Thursday)
**Chat Session Heartbeat and User Settings**
This week focused on implementing chat session heartbeat functionality and user settings management, ensuring that active sessions are properly tracked and can be recovered in case of unexpected disconnections.
### Changes:
- Implemented chat session heartbeat and session unlock
- Added user settings management
- Made reasoning effort configurable
- Added editable session name with cog icon
- Redesigned sign-in form and presentation
- Integrated persona field in User
- Reworked provider/model selection with save/cancel pattern
- Added navigation to return to Project Manager
- Quick fix for when logging.file.enabled is false
- Refactor: unify logging into @gadget/api as GadgetLog
---
## 2026-05-07 (Wednesday)
**Streaming Responses Implementation**
This week saw the implementation of streaming responses for AI model interactions, including proper handling of streaming data from Ollama and other providers with support for real-time token delivery and response completion tracking.
### Changes:
- Implemented streaming responses for Ollama
- Added streaming response handling and correctness improvements
- Created documentation for streaming responses
- Improved real-time token delivery and completion tracking
---
## 2026-05-06 (Tuesday)
**Agent Tool and Toolbox Refactoring**
This week involved significant refactoring of the agent tool and toolbox architecture, including unifying logging into @gadget/api as GadgetLog and restructuring the toolbox to support better tool registration and management.
### Changes:
- Refactored agent tool and toolbox architecture
- Unified logging into @gadget/api as GadgetLog
- Improved tool registration and management
- Reduced log spam through pre-task cleanup
- Agent, tools, toolbox, tool loop, AI environment
---
## 2026-05-05 (Monday)
**Basic Chat System Implementation**
This week marked a major milestone with the implementation of a working basic chat system, including provider, model, and mode selection capabilities, along with enhanced AgentService.process method functionality.
### Changes:
- Implemented basic chat system
- Added provider, model, and mode selections
- Enhanced AgentService.process method
- Implemented chat turn processing
- Added agent progress tracking
---
## 2026-05-03 (Saturday)
**Status Message Display Fix**
This weekend's work focused on fixing the status message display in the agent interface and implementing workspace mode management improvements with drone status message socket events.
### Changes:
- Fixed status message display in agent interface
- Implemented workspace mode management
- Added drone status message socket events
- Many fixes
---
## 2026-05-02 (Friday)
**Workspace Mode Switching**
This week involved implementing workspace mode switching functionality with proper validation, ensuring that workspace mode changes are validated before being applied, preventing invalid state transitions.
### Changes:
- Implemented workspace mode switching with validation
- Added session lock and workspace processing refinements
- Improved workspace management system
- Workspace management progress
---
## 2026-05-01 (Thursday)
**Welcome to The Grid**
This week marked the beginning of a new phase with the implementation of the "Welcome to The Grid" home view, including JWT processing fixes, session startup/shutdown/refactoring improvements, and the transition from Types.ObjectId to GadgetId.
### Changes:
- Implemented "Welcome to The Grid" home view
- Fixed JWT processing
- Refactored session startup, shutdown, and import
- Transitioned from Types.ObjectId to GadgetId
- Added return to Home on sign out
- More populate fixes
---
## 2026-04-30 (Wednesday)
**Socket Protocol Implementation**
This week focused on implementing and refining the socket protocol for drone-to-IDE communication, including defining missing socket event types, enforcing typed events in the frontend build, and adding a setting for max Socket.IO HTTP buffer size.
### Changes:
- Implemented socket protocol completeness
- Defined missing socket event types
- Enforced typed events in frontend build
- Added max Socket.IO HTTP buffer size setting
- Created drone manager
---
## 2026-04-29 (Tuesday)
**Workspace Persistence and Crash Recovery**
This week involved implementing workspace persistence and crash recovery capabilities, including adding workspace mode to the gadget-drone process, event emissions to the Agentic Workflow Loop, and drone-to-IDE event routing.
### Changes:
- Implemented workspace persistence and crash recovery
- Added workspace mode to gadget-drone process
- Added event emissions to Agentic Workflow Loop
- Implemented drone-to-IDE event routing
- Fixed type conflicts and implemented prompt submission
---
## 2026-04-28 (Monday)
**Dark Industrial Theme and JWT Authentication**
This week marked a significant visual and architectural milestone with the implementation of the dark industrial theme, Project Manager, and JWT authentication, replacing the previous sign-up concept with a more streamlined sign-in experience.
### Changes:
- Implemented dark industrial theme
- Added Project Manager
- Implemented JWT authentication
- Enforced theme correctly
- Added project service
- Updated DroneRegistration with workspaceDir
- Fix: Darken border colors to match dark industrial theme
- Theme enforced and correctly implemented; e2e tests added for theme
---
## 2026-04-27 (Sunday)
**Initial Commit**
The project was initialized with the initial commit, establishing the foundation for all subsequent development.
### Changes:
- Initial project commit
- Established basic project structure

View File

@ -16,33 +16,27 @@ or is aborted, the abort signal mechanism is cleaned up end-to-end.
```
[Frontend] --abortWorkOrder(cb)--> [CodeSession] --abortWorkOrder(cb)--> [Drone]
|
AgentService
.abortCurrentWorkOrder()
|
abortController.abort()
|
AiService.abort()
(calls AI provider's
abort() which for
Ollama terminates the
HTTP request immediately)
|
cb(true, "Abort signaled")
|
|
AgentService
.abortCurrentWorkOrder()
|
abortController.abort()
|
cb(true, "Abort signaled")
|
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
(AbortError thrown
in AI provider)
|
AgentService catches
AbortError, emits
workOrderComplete(turnId, true, "aborted")
|
|
(AbortError thrown
in AI provider)
|
AgentService catches
AbortError, emits
workOrderComplete(turnId, true, "aborted")
|
[DroneSession] --workOrderComplete(turnId, true, "aborted")--> [CodeSession]
| |
turn.status = turn.status = "aborted"
ChatTurnStatus.Aborted displayed subtly
| |
turn.status = turn.status = "aborted"
ChatTurnStatus.Aborted displayed subtly
```
**Key design decision**: `workOrderComplete` is used with `success=true` and
@ -80,10 +74,6 @@ No `SocketEvents` changes needed — the drone listens on its own socket.
- `IAiChatOptions`
- `IAiGenerateOptions`
Also added a default (no-op) `abort()` method on the `AiApi` abstract class.
Provider-specific implementations override this to forcefully terminate
in-progress HTTP requests.
**`packages/ai/src/ollama.ts`** — In both `generate()` and `chat()`:
1. Pre-request check: `if (options.signal?.aborted) throw ...`
@ -91,9 +81,6 @@ in-progress HTTP requests.
3. Per-chunk check at top of `for await` loop:
`if (options.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError")`
Additionally, `OllamaAiApi` overrides `abort()` to call `this.client.abort()`,
which immediately terminates the underlying HTTP request to the Ollama server.
**`packages/ai/src/openai.ts`** — Same pattern in:
- `generate()` — pre-check, pass `signal` to SDK, per-chunk in `for await`
@ -121,18 +108,7 @@ and is detected downstream via `error.name === "AbortError"`.
```
- `finally` block sets `this.abortController = null`
- Public method `abortCurrentWorkOrder(): boolean` — calls
`this.abortController?.abort()` then `AiService.abort()`, returns `true` if
there was a controller. The `AiService.abort()` call provides provider-specific
forceful abort (e.g., `OllamaAiApi.abort()``this.client.abort()`).
**`gadget-drone/src/services/ai.ts`**:
- Property: `private activeApi: AiApi | null = null` — tracks the most recently
created `AiApi` instance
- `getApi()` stores each created instance on `this.activeApi`
- `chat()` and `generate()` clear `this.activeApi = null` in `finally` blocks
- Public method `abort(): void` — calls `this.activeApi?.abort()`, then clears
the reference
`this.abortController?.abort()`, returns `true` if there was a controller
**`gadget-drone/src/gadget-drone.ts`**:
@ -217,11 +193,10 @@ status: "processing" | "finished" | "aborted" | "error";
| `.gitignore` | Add root-level log file patterns |
| `packages/api/src/messages/ide.ts` | New `AbortWorkOrderCallback`, `AbortWorkOrderMessage` |
| `packages/api/src/messages/socket.ts` | Register `abortWorkOrder` in both event interfaces |
| `packages/ai/src/api.ts` | `signal?: AbortSignal` on `IAiChatOptions`, `IAiGenerateOptions`; `abort()` on `AiApi` |
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through; `abort()` override |
| `packages/ai/src/api.ts` | `signal?: AbortSignal` on `IAiChatOptions`, `IAiGenerateOptions` |
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through |
| `packages/ai/src/openai.ts` | Same for `generate()`, `readStreamingChatCompletion()`, `readNonStreamingChatCompletion()` |
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` calls `AiService.abort()` |
| `gadget-drone/src/services/ai.ts` | Track active `AiApi` instance, `abort()` method, cleanup in `finally` blocks |
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` |
| `gadget-drone/src/gadget-drone.ts` | `onAbortWorkOrder` socket handler |
| `gadget-code/src/lib/code-session.ts` | Forward `abortWorkOrder` to drone |
| `gadget-code/src/lib/drone-session.ts` | Detect `"aborted"` in `workOrderComplete` |
@ -239,16 +214,9 @@ status: "processing" | "finished" | "aborted" | "error";
event or change the `workOrderComplete` signature.
- The `AbortController` is created per-process() call and cleaned up in
`finally`. It is never shared across work orders.
- The abort callback returns immediately after the controller is signaled and
`AiService.abort()` is called. The AbortError propagates asynchronously
through the SDK stream and is caught separately in the agent loop.
- `AiService.abort()` provides provider-specific forceful abort (e.g., Ollama's
`client.abort()` terminates the HTTP request immediately). This is called
_in addition to_ the `AbortController.abort()` signal so that both the
AbortSignal chain and the provider-specific abort mechanism are triggered.
- The active `AiApi` instance is tracked on `AiService.activeApi` and is set to
`null` in the `finally` block of each `chat()`/`generate()` call, so the
reference is never stale.
- The abort callback returns immediately after the controller is signaled. The
AbortError propagates asynchronously through the SDK stream and is caught
separately in the agent loop.
- Subagent loops also receive the abort signal (via `this.abortController?.signal`)
and abort naturally — no special subagent abort handling needed.
- The Esc handler is a global `window` keydown listener. It is registered ONLY

View File

@ -1,43 +0,0 @@
import type { Project } from '../lib/api';
interface ProjectPanelProps {
project: Project | null;
onNavigateToProject: () => void;
}
export default function ProjectPanel({ project, onNavigateToProject }: ProjectPanelProps) {
return (
<div className="border-b border-border-subtle shrink-0">
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
Project
</h3>
<button
title="Project Manager"
onClick={onNavigateToProject}
className="text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</button>
</div>
<div className="p-4 space-y-2">
{project ? (
<>
<div>
<div className="text-xs text-text-muted">Name</div>
<div className="text-text-primary">{project.name}</div>
</div>
<div>
<div className="text-xs text-text-muted">Slug</div>
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
</div>
</>
) : (
<div className="text-text-muted text-sm">Loading...</div>
)}
</div>
</div>
);
}

View File

@ -1,298 +0,0 @@
import { ChatSessionMode, type AiProvider, type ChatSession, type AiModel } from '../lib/api';
import { WorkspaceMode } from '../lib/types';
import WorkspaceModeIndicator from './WorkspaceModeIndicator';
interface SessionPanelProps {
session: ChatSession | null;
workspaceMode: WorkspaceMode;
sessionLocked: boolean;
providers: AiProvider[];
selectedProviderId: string;
selectedModelId: string;
sessionReasoningEffort: string;
sessionNumCtx: number;
isEditingProvider: boolean;
editProviderId: string;
editModelId: string;
isUpdatingProvider: boolean;
isEditingName: boolean;
editName: string;
isUpdatingName: boolean;
isUpdatingMode: boolean;
isUpdatingReasoning: boolean;
isUpdatingNumCtx: boolean;
onWorkspaceModeChange: (mode: WorkspaceMode) => void;
onModeChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
onReasoningChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
onNumCtxChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onNumCtxCommit: (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => void;
onStartEditProvider: () => void;
onSaveProvider: () => void;
onCancelEditProvider: () => void;
onEditProviderIdChange: (id: string) => void;
onEditModelIdChange: (id: string) => void;
onStartEditName: () => void;
onSaveName: () => void;
onCancelEditName: () => void;
onEditNameChange: (name: string) => void;
getProviderName: (id: string) => string;
getModelName: (providerId: string, modelId: string) => string;
getSortedModels: (providerId: string) => AiProvider['models'];
truncateModelName: (name: string, maxLength?: number) => string;
getSelectedModelCapabilities: () => { hasThinking: boolean } | null;
getSelectedModel: () => AiModel | null;
}
export default function SessionPanel({
session,
workspaceMode,
sessionLocked,
providers,
selectedProviderId,
selectedModelId,
sessionReasoningEffort,
sessionNumCtx,
isEditingProvider,
editProviderId,
editModelId,
isUpdatingProvider,
isEditingName,
editName,
isUpdatingName,
isUpdatingMode,
isUpdatingReasoning,
isUpdatingNumCtx,
onWorkspaceModeChange,
onModeChange,
onReasoningChange,
onNumCtxChange,
onNumCtxCommit,
onStartEditProvider,
onSaveProvider,
onCancelEditProvider,
onEditProviderIdChange,
onEditModelIdChange,
onStartEditName,
onSaveName,
onCancelEditName,
onEditNameChange,
getProviderName,
getModelName,
getSortedModels,
truncateModelName,
getSelectedModelCapabilities,
getSelectedModel,
}: SessionPanelProps) {
return (
<div className="border-b border-border-subtle shrink-0">
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
Session
</h3>
<WorkspaceModeIndicator
mode={workspaceMode}
onChange={onWorkspaceModeChange}
disabled={!sessionLocked || isEditingProvider}
/>
</div>
<div className="p-4 space-y-3">
<div>
<div className="text-xs text-text-muted">Name</div>
{isEditingName ? (
<div className="flex gap-2 mt-1">
<input
type="text"
value={editName}
onChange={(e) => onEditNameChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') { e.preventDefault(); onSaveName(); }
if (e.key === 'Escape') { e.preventDefault(); onCancelEditName(); }
}}
className="flex-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
autoFocus
/>
<button
title="Save"
onClick={onSaveName}
disabled={!editName.trim() || isUpdatingName}
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors self-center"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
title="Cancel"
onClick={onCancelEditName}
disabled={isUpdatingName}
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors self-center"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
) : (
<div className="flex gap-2 items-center mt-1">
<div className="text-text-primary flex-1">{session?.name}</div>
<button
title="Edit name"
onClick={onStartEditName}
className="text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
)}
</div>
{isEditingProvider ? (
<div className="flex gap-2">
<div className="flex-1">
<div className="text-xs text-text-muted">Provider</div>
<select
value={editProviderId || ''}
onChange={(e) => { onEditProviderIdChange(e.target.value); onEditModelIdChange(''); }}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
>
<option value="">-- Select Provider --</option>
{providers.map((provider) => (
<option key={provider._id} value={provider._id}>
{provider.name}
</option>
))}
</select>
</div>
<div className="flex-1">
<div className="text-xs text-text-muted">Model</div>
<select
value={editModelId || ''}
onChange={(e) => onEditModelIdChange(e.target.value)}
disabled={!editProviderId}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed truncate"
>
{!editProviderId ? (
<option value="">Select provider first</option>
) : (
<>
<option value="">-- Select Model --</option>
{getSortedModels(editProviderId).map((model) => (
<option key={model.id} value={model.id}>
{truncateModelName(model.name)}
</option>
))}
</>
)}
</select>
</div>
<div className="flex items-end gap-1 pb-1">
<button
title="Save"
onClick={onSaveProvider}
disabled={!editProviderId || !editModelId || isUpdatingProvider}
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
title="Cancel"
onClick={onCancelEditProvider}
disabled={isUpdatingProvider}
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
) : (
<div className="flex gap-2">
<div className="flex-1">
<div className="text-xs text-text-muted">Provider</div>
<div className="text-text-primary text-sm mt-1">{getProviderName(selectedProviderId)}</div>
</div>
<div className="flex-1">
<div className="text-xs text-text-muted">Model</div>
<div
className="text-text-primary text-sm mt-1 truncate"
title={getModelName(selectedProviderId, selectedModelId)}
>
{getModelName(selectedProviderId, selectedModelId)}
</div>
</div>
<button
title="Change provider/model..."
onClick={onStartEditProvider}
className="self-end pb-1 text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
)}
<div>
<div className="text-xs text-text-muted">Mode</div>
<select
value={session?.mode || ChatSessionMode.Build}
onChange={onModeChange}
disabled={isUpdatingMode}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{Object.values(ChatSessionMode).map((mode) => (
<option key={mode} value={mode}>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</option>
))}
</select>
</div>
<div>
<div className="text-xs text-text-muted">Reasoning</div>
<select
value={sessionReasoningEffort}
onChange={onReasoningChange}
disabled={isUpdatingReasoning || !getSelectedModelCapabilities()?.hasThinking}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
title={
!getSelectedModelCapabilities()?.hasThinking
? "Selected model does not support reasoning"
: "Controls how much the model thinks before responding"
}
>
<option value="off">Off</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
{providers.find(p => p._id === selectedProviderId)?.apiType !== 'openai' && (
<div>
<div className="text-xs text-text-muted">Context Window</div>
<div className="flex items-center gap-2 mt-1">
<input
type="range"
min={16384}
max={getSelectedModel()?.contextWindow || 131072}
step={8192}
value={sessionNumCtx}
onChange={onNumCtxChange}
onMouseUp={onNumCtxCommit}
onTouchEnd={onNumCtxCommit}
className="flex-1 h-2 bg-bg-tertiary rounded appearance-none cursor-pointer accent-brand"
/>
<span className="text-xs text-text-secondary w-16 text-right tabular-nums">
{sessionNumCtx.toLocaleString()}
</span>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -358,7 +358,6 @@ export interface ChatSession {
provider: string | AiProvider;
selectedModel: string;
reasoningEffort?: string;
numCtx?: number;
stats: ChatSessionStats;
pins: Array<{ _id?: string; content: string }>;
}
@ -422,7 +421,6 @@ export interface ChatTurn {
session: string | ChatSession;
provider: string | AiProvider;
llm: string;
numCtx?: number;
mode: ChatSessionMode;
status: "processing" | "finished" | "aborted" | "error";
prompts: ChatTurnPrompts;

View File

@ -3,8 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { socketClient } from '../lib/socket';
import { chatSessionApi, projectApi, providerApi, type ChatSession, type ChatTurn, type ChatTurnBlock, ChatTurnStats, ChatSessionMode, type AiProvider, type Project } from '../lib/api';
import { WorkspaceMode } from '../lib/types';
import SessionPanel from '../components/SessionPanel';
import ProjectPanel from '../components/ProjectPanel';
import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
import FilesPanel from '../components/FilesPanel';
import LogPanel from '../components/LogPanel';
import ChatTurnComponent from '../components/ChatTurn';
@ -68,8 +67,6 @@ export default function ChatSessionView() {
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
const [selectedModelId, setSelectedModelId] = useState<string>('');
const [sessionReasoningEffort, setSessionReasoningEffort] = useState<string>('off');
const [sessionNumCtx, setSessionNumCtx] = useState<number>(131072);
const [isUpdatingNumCtx, setIsUpdatingNumCtx] = useState(false);
const [isEditingProvider, setIsEditingProvider] = useState(false);
const [editProviderId, setEditProviderId] = useState('');
const [editModelId, setEditModelId] = useState('');
@ -216,15 +213,6 @@ export default function ChatSessionView() {
setSelectedProviderId(providerId || '');
setSelectedModelId(sessionData.selectedModel || '');
setSessionReasoningEffort(sessionData.reasoningEffort || 'off');
// Initialize numCtx: persisted value wins, otherwise model's contextWindow
if (sessionData.numCtx) {
setSessionNumCtx(sessionData.numCtx);
} else {
const provider = allProviders.find(p => p._id === providerId);
const model = provider?.models.find(m => m.id === sessionData.selectedModel);
setSessionNumCtx(model?.contextWindow || 131072);
}
// Set connection state to connecting - will update on socket connect
setConnectionState('connecting');
@ -804,16 +792,13 @@ export default function ChatSessionView() {
}
};
const getSelectedModel = useCallback(() => {
const getSelectedModelCapabilities = useCallback(() => {
const provider = providers.find(p => p._id === selectedProviderId);
if (!provider) return null;
return provider.models.find(m => m.id === selectedModelId) || null;
const model = provider.models.find(m => m.id === selectedModelId);
return model?.capabilities || null;
}, [providers, selectedProviderId, selectedModelId]);
const getSelectedModelCapabilities = useCallback(() => {
return getSelectedModel()?.capabilities || null;
}, [getSelectedModel]);
const handleReasoningChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
if (!session) return;
@ -835,28 +820,6 @@ export default function ChatSessionView() {
}
};
const handleNumCtxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSessionNumCtx(parseInt(e.target.value, 10));
};
const handleNumCtxCommit = async (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => {
if (!session || isUpdatingNumCtx) return;
const newValue = parseInt(e.currentTarget.value, 10);
setIsUpdatingNumCtx(true);
try {
const updatedSession = await chatSessionApi.update(session._id, {
numCtx: newValue
});
setSession(updatedSession);
showToast(`Context window set to ${newValue.toLocaleString()}`);
} catch (err) {
showToast(`Failed to change context window: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setIsUpdatingNumCtx(false);
}
};
const getProviderName = (id: string) => {
return providers.find(p => p._id === id)?.name ?? 'None';
};
@ -895,13 +858,6 @@ export default function ChatSessionView() {
setSession(updatedSession);
setSelectedProviderId(editProviderId);
setSelectedModelId(editModelId);
// Reset numCtx to new model's max context window
const newProvider = providers.find(p => p._id === editProviderId);
const newModel = newProvider?.models.find(m => m.id === editModelId);
const newMaxCtx = newModel?.contextWindow || 131072;
setSessionNumCtx(newMaxCtx);
setIsEditingProvider(false);
showToast('Provider and model updated');
} catch (err) {
@ -1208,52 +1164,232 @@ export default function ChatSessionView() {
{/* Sidebar */}
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col">
<SessionPanel
session={session}
workspaceMode={workspaceMode}
sessionLocked={sessionLocked}
providers={providers}
selectedProviderId={selectedProviderId}
selectedModelId={selectedModelId}
sessionReasoningEffort={sessionReasoningEffort}
sessionNumCtx={sessionNumCtx}
isEditingProvider={isEditingProvider}
editProviderId={editProviderId}
editModelId={editModelId}
isUpdatingProvider={isUpdatingProvider}
isEditingName={isEditingName}
editName={editName}
isUpdatingName={isUpdatingName}
isUpdatingMode={isUpdatingMode}
isUpdatingReasoning={isUpdatingReasoning}
isUpdatingNumCtx={isUpdatingNumCtx}
onWorkspaceModeChange={handleWorkspaceModeChange}
onModeChange={handleModeChange}
onReasoningChange={handleReasoningChange}
onNumCtxChange={handleNumCtxChange}
onNumCtxCommit={handleNumCtxCommit}
onStartEditProvider={handleStartEditProvider}
onSaveProvider={handleSaveProvider}
onCancelEditProvider={handleCancelEditProvider}
onEditProviderIdChange={setEditProviderId}
onEditModelIdChange={setEditModelId}
onStartEditName={handleStartEditName}
onSaveName={handleSaveName}
onCancelEditName={handleCancelEditName}
onEditNameChange={setEditName}
getProviderName={getProviderName}
getModelName={getModelName}
getSortedModels={getSortedModels}
truncateModelName={truncateModelName}
getSelectedModelCapabilities={getSelectedModelCapabilities}
getSelectedModel={getSelectedModel}
/>
{/* SESSION Panel */}
<div className="border-b border-border-subtle shrink-0">
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
Session
</h3>
<WorkspaceModeIndicator
mode={workspaceMode}
onChange={handleWorkspaceModeChange}
disabled={!sessionLocked || isEditingProvider}
/>
</div>
<div className="p-4 space-y-3">
<div>
<div className="text-xs text-text-muted">Name</div>
{isEditingName ? (
<div className="flex gap-2 mt-1">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') { e.preventDefault(); handleSaveName(); }
if (e.key === 'Escape') { e.preventDefault(); handleCancelEditName(); }
}}
className="flex-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
autoFocus
/>
<button
title="Save"
onClick={handleSaveName}
disabled={!editName.trim() || isUpdatingName}
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors self-center"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
title="Cancel"
onClick={handleCancelEditName}
disabled={isUpdatingName}
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors self-center"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
) : (
<div className="flex gap-2 items-center mt-1">
<div className="text-text-primary flex-1">{session?.name}</div>
<button
title="Edit name"
onClick={handleStartEditName}
className="text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
)}
</div>
{isEditingProvider ? (
<div className="flex gap-2">
<div className="flex-1">
<div className="text-xs text-text-muted">Provider</div>
<select
value={editProviderId || ''}
onChange={(e) => { setEditProviderId(e.target.value); setEditModelId(''); }}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
>
<option value="">-- Select Provider --</option>
{providers.map((provider) => (
<option key={provider._id} value={provider._id}>
{provider.name}
</option>
))}
</select>
</div>
<div className="flex-1">
<div className="text-xs text-text-muted">Model</div>
<select
value={editModelId || ''}
onChange={(e) => setEditModelId(e.target.value)}
disabled={!editProviderId}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed truncate"
>
{!editProviderId ? (
<option value="">Select provider first</option>
) : (
<>
<option value="">-- Select Model --</option>
{getSortedModels(editProviderId).map((model) => (
<option key={model.id} value={model.id}>
{truncateModelName(model.name)}
</option>
))}
</>
)}
</select>
</div>
<div className="flex items-end gap-1 pb-1">
<button
title="Save"
onClick={handleSaveProvider}
disabled={!editProviderId || !editModelId || isUpdatingProvider}
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
title="Cancel"
onClick={handleCancelEditProvider}
disabled={isUpdatingProvider}
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
) : (
<div className="flex gap-2">
<div className="flex-1">
<div className="text-xs text-text-muted">Provider</div>
<div className="text-text-primary text-sm mt-1">{getProviderName(selectedProviderId)}</div>
</div>
<div className="flex-1">
<div className="text-xs text-text-muted">Model</div>
<div
className="text-text-primary text-sm mt-1 truncate"
title={getModelName(selectedProviderId, selectedModelId)}
>
{getModelName(selectedProviderId, selectedModelId)}
</div>
</div>
<button
title="Change provider/model..."
onClick={handleStartEditProvider}
className="self-end pb-1 text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
)}
<div>
<div className="text-xs text-text-muted">Mode</div>
<select
value={session?.mode || ChatSessionMode.Build}
onChange={handleModeChange}
disabled={isUpdatingMode}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{Object.values(ChatSessionMode).map((mode) => (
<option key={mode} value={mode}>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</option>
))}
</select>
</div>
<div>
<div className="text-xs text-text-muted">Reasoning</div>
<select
value={sessionReasoningEffort}
onChange={handleReasoningChange}
disabled={isUpdatingReasoning || !getSelectedModelCapabilities()?.hasThinking}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
title={
!getSelectedModelCapabilities()?.hasThinking
? "Selected model does not support reasoning"
: "Controls how much the model thinks before responding"
}
>
<option value="off">Off</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
</div>
<ProjectPanel
project={project}
onNavigateToProject={() => navigate(`/projects/${project?.slug}`)}
/>
{/* PROJECT Panel */}
<div className="border-b border-border-subtle shrink-0">
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
Project
</h3>
<button
title="Project Manager"
onClick={() => navigate(`/projects/${project?.slug}`)}
className="text-text-muted hover:text-text-primary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</button>
</div>
<div className="p-4 space-y-2">
{project ? (
<>
<div>
<div className="text-xs text-text-muted">Name</div>
<div className="text-text-primary">{project.name}</div>
</div>
<div>
<div className="text-xs text-text-muted">Slug</div>
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
</div>
</>
) : (
<div className="text-text-muted text-sm">Loading...</div>
)}
</div>
</div>
{/* FILES Panel */}
<FilesPanel
workspaceMode={workspaceMode}
onFileSelect={(path) => {

View File

@ -12,7 +12,6 @@ import {
providerApi,
} from "../lib/api";
import { socketClient } from "../lib/socket";
import gabAiImage from "./assets/gab-ai.jpeg";
interface ProjectManagerProps {
user: User | null;
@ -232,11 +231,7 @@ interface ProjectInspectorProps {
onUpdate: () => void;
}
function ProjectInspector({
project,
onDelete,
onUpdate,
}: ProjectInspectorProps) {
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
const [deleting, setDeleting] = useState(false);
const [editing, setEditing] = useState(false);
@ -284,9 +279,7 @@ function ProjectInspector({
</div>
<div className="p-4 bg-bg-secondary border border-border-default rounded">
<div className="text-sm text-text-muted mb-1">Slug</div>
<div className="font-mono text-text-primary">
{project.slug}
</div>
<div className="font-mono text-text-primary">{project.slug}</div>
</div>
</div>
@ -659,152 +652,118 @@ function NewChatSessionModal({
}
};
const affiliateHref = "https://gab.ai/?ref=c7182f6e";
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-2xl flex gap-6">
<div className="w-48 flex-shrink-0 flex flex-col">
<a
href={affiliateHref}
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0"
>
<img src={gabAiImage} alt="Gab AI" className="w-full rounded" />
</a>
<p className="flex-1 flex items-center justify-center text-center text-lg text-text-secondary px-1">
Need big-brain inference for this session?
</p>
<p className="flex-1 flex items-center justify-center text-left text-sm text-text-secondary px-1">
Sign up for Gab AI and get 250 credits to see if it's right for you!
</p>
<a
href={affiliateHref}
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0"
>
<button className="w-full px-4 py-2 border border-[#00d178] text-[#00d178] rounded hover:bg-[#00d178] hover:text-white text-sm font-medium transition-colors">
Get Started
</button>
</a>
</div>
<div className="flex-1 border-l border-border-subtle pl-6">
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
{selectedDrone && (
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
<div className="text-xs text-text-muted mb-1">Selected Drone</div>
<div className="font-mono text-sm text-text-primary">
{selectedDrone.hostname}
</div>
<div className="text-xs text-text-muted truncate">
{selectedDrone.workspaceDir}
</div>
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
{selectedDrone && (
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
<div className="text-xs text-text-muted mb-1">Selected Drone</div>
<div className="font-mono text-sm text-text-primary">
{selectedDrone.hostname}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-text-secondary mb-1">
Session Name (optional)
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
placeholder="Auto-generated from first prompt"
/>
<div className="text-xs text-text-muted truncate">
{selectedDrone.workspaceDir}
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-text-secondary mb-1">
Session Name (optional)
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
placeholder="Auto-generated from first prompt"
/>
</div>
<div>
<label className="block text-sm text-text-secondary mb-1">
AI Provider *
</label>
<select
value={selectedProviderId}
onChange={(e) => {
setSelectedProviderId(e.target.value);
setSelectedModel("");
}}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
>
<option value="">Select a provider</option>
{providers.map((provider) => (
<option key={provider._id} value={provider._id}>
{provider.name} ({provider.apiType})
</option>
))}
</select>
</div>
{selectedProvider && (
<div>
<label className="block text-sm text-text-secondary mb-1">
AI Provider *
Model *
</label>
<select
value={selectedProviderId}
onChange={(e) => {
setSelectedProviderId(e.target.value);
setSelectedModel("");
}}
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
disabled={selectedProvider.models.length === 0}
>
<option value="">Select a provider</option>
{providers.map((provider) => (
<option key={provider._id} value={provider._id}>
{provider.name} ({provider.apiType})
<option value="">Select a model</option>
{[...selectedProvider.models].sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
<option key={model.id} value={model.id}>
{model.name}{" "}
{model.parameterLabel ? `(${model.parameterLabel})` : ""}
</option>
))}
</select>
{selectedProvider.models.length === 0 && (
<p className="text-xs text-text-muted mt-1">
No models discovered. Run `pnpm cli provider probe{" "}
{selectedProvider._id}` to discover models.
</p>
)}
</div>
)}
{selectedProvider && (
<div>
<label className="block text-sm text-text-secondary mb-1">
Model *
</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
disabled={selectedProvider.models.length === 0}
>
<option value="">Select a model</option>
{[...selectedProvider.models]
.sort((a, b) => a.name.localeCompare(b.name))
.map((model) => (
<option key={model.id} value={model.id}>
{model.name}{" "}
{model.parameterLabel
? `(${model.parameterLabel})`
: ""}
</option>
))}
</select>
{selectedProvider.models.length === 0 && (
<p className="text-xs text-text-muted mt-1">
No models discovered. Run `pnpm cli provider probe{" "}
{selectedProvider._id}` to discover models.
</p>
)}
</div>
)}
<div>
<label className="block text-sm text-text-secondary mb-1">
Mode
</label>
<select
value={mode}
onChange={(e) => setMode(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
>
<option value="plan">Plan - Planning and brainstorming</option>
<option value="build">Build - Building and coding</option>
<option value="test">Test - Testing and debugging</option>
<option value="ship">Ship - Finalizing and shipping</option>
<option value="dev">Dev - Working on Gadget Code itself</option>
</select>
</div>
<div>
<label className="block text-sm text-text-secondary mb-1">
Mode
</label>
<select
value={mode}
onChange={(e) => setMode(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
>
<option value="plan">Plan - Planning and brainstorming</option>
<option value="build">Build - Building and coding</option>
<option value="test">Test - Testing and debugging</option>
<option value="ship">Ship - Finalizing and shipping</option>
<option value="dev">Dev - Working on Gadget Code itself</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={creating || !selectedProviderId || !selectedModel}
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 flex-1"
>
{creating ? "Creating..." : "Create Session"}
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={creating || !selectedProviderId || !selectedModel}
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 flex-1"
>
{creating ? "Creating..." : "Create Session"}
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
@ -815,9 +774,7 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
const { slug } = useParams();
const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(
null,
);
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
const [loading, setLoading] = useState(true);
const [showNewForm, setShowNewForm] = useState(false);
@ -882,10 +839,7 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
console.error("Failed to lock drone session");
return;
}
localStorage.setItem(
"dtp_drone_registration",
JSON.stringify(selectedDrone),
);
localStorage.setItem('dtp_drone_registration', JSON.stringify(selectedDrone));
navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`);
} catch (err) {
console.error("Failed to open chat session", err);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

View File

@ -1,29 +0,0 @@
declare module "*.jpeg" {
const src: string;
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.gif" {
const src: string;
export default src;
}
declare module "*.svg" {
const src: string;
export default src;
}
declare module "*.webp" {
const src: string;
export default src;
}

View File

@ -179,7 +179,6 @@ class ChatSessionController extends DtpController {
selectedModel: string;
mode: ChatSessionMode;
reasoningEffort: "off" | "low" | "medium" | "high";
numCtx: number;
}> = {};
if (updates.name !== undefined) {
@ -205,12 +204,6 @@ class ChatSessionController extends DtpController {
}
allowedUpdates.reasoningEffort = updates.reasoningEffort;
}
if (updates.numCtx !== undefined) {
if (typeof updates.numCtx !== "number" || !Number.isInteger(updates.numCtx) || updates.numCtx < 16384 || updates.numCtx % 8192 !== 0) {
throw new Error(`Invalid numCtx: ${updates.numCtx}. Must be an integer >= 16384 and a multiple of 8192.`);
}
allowedUpdates.numCtx = updates.numCtx;
}
const session = await ChatSessionService.update(
res.locals.chatSession._id,

View File

@ -30,7 +30,6 @@ export const ChatSessionSchema = new Schema<IChatSession>({
enum: ["off", "low", "medium", "high"],
default: "off",
},
numCtx: { type: Number },
stats: {
turnCount: { type: Number, default: 0, required: true },
toolCallCount: { type: Number, default: 0, required: true },

View File

@ -69,7 +69,6 @@ export const ChatTurnSchema = new Schema<IChatTurn>({
enum: ["off", "low", "medium", "high"],
default: "off",
},
numCtx: { type: Number },
mode: {
type: String,
enum: ChatSessionMode,

View File

@ -202,7 +202,6 @@ class ChatSessionService extends DtpService {
selectedModel: string;
mode: ChatSessionMode;
reasoningEffort: ReasoningEffort;
numCtx: number;
}>,
): Promise<IChatSession> {
const session = await ChatSession.findById(chatSessionId);
@ -231,9 +230,6 @@ class ChatSessionService extends DtpService {
if (updates.reasoningEffort !== undefined) {
session.reasoningEffort = updates.reasoningEffort;
}
if (updates.numCtx !== undefined) {
session.numCtx = updates.numCtx;
}
await session.save();
@ -280,7 +276,6 @@ class ChatSessionService extends DtpService {
provider: session.provider,
llm: session.selectedModel,
reasoningEffort: (session as IChatSession).reasoningEffort || "off",
numCtx: (session as IChatSession).numCtx,
mode: session.mode,
status: ChatTurnStatus.Processing,
prompts: {

View File

@ -218,7 +218,7 @@ class AgentService extends GadgetService {
try {
response = await AiService.chat(
turn.provider,
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning, turn.numCtx),
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning),
chatOptions,
this.makeStreamHandler(socket),
);
@ -337,7 +337,6 @@ class AgentService extends GadgetService {
abortCurrentWorkOrder(): boolean {
if (!this.abortController) return false;
this.abortController.abort();
AiService.abort();
return true;
}
@ -474,7 +473,6 @@ class AgentService extends GadgetService {
provider: IAiProvider | GadgetId,
modelId: string,
reasoning: boolean | "low" | "medium" | "high",
numCtxOverride?: number,
): Omit<IDroneModelConfig, "provider"> {
const settings = this.getModelSettings(provider, modelId);
return {
@ -485,7 +483,7 @@ class AgentService extends GadgetService {
topP: settings?.topP ?? 0.9,
topK: settings?.topK ?? 40,
numPredict: settings?.numPredict ?? -1, // -1 = unlimited (Ollama)
numCtx: numCtxOverride ?? settings?.numCtx ?? 131072, // session override > model settings > 128k fallback
numCtx: settings?.numCtx ?? 131072, // 128k fallback
maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback
},
};
@ -627,7 +625,7 @@ class AgentService extends GadgetService {
const response = await AiService.chat(
provider,
this.buildDroneModelConfig(provider, turn.llm, false, turn.numCtx),
this.buildDroneModelConfig(provider, turn.llm, false),
chatOptions,
streamHandler,
);

View File

@ -18,7 +18,6 @@ const aiEnv: IAiEnvironment = {
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
import { GadgetService } from "../lib/service.js";
import {
AiApi,
type IAiChatOptions,
type IAiChatResponse,
type IAiGenerateOptions,
@ -48,8 +47,6 @@ class AiService extends GadgetService {
return "svc:ai";
}
private activeApi: AiApi | null = null;
async start(): Promise<void> {
this.log.info("started");
}
@ -106,11 +103,7 @@ class AiService extends GadgetService {
});
const api = this.getApi(config);
const modelConfig: IAiModelConfig = { ...model, provider: config };
try {
return await api.generate(modelConfig, options, streamCallback);
} finally {
this.activeApi = null;
}
return api.generate(modelConfig, options, streamCallback);
}
async chat(
@ -128,23 +121,11 @@ class AiService extends GadgetService {
});
const api = this.getApi(config);
const modelConfig: IAiModelConfig = { ...model, provider: config };
try {
return await api.chat(modelConfig, options, streamCallback);
} finally {
this.activeApi = null;
}
return await api.chat(modelConfig, options, streamCallback);
}
getApi(provider: AiProviderConfig) {
const api = createAiApi(aiEnv, provider, this.log);
this.activeApi = api;
return api;
}
/** Forcefully abort any in-progress request on the active API instance. */
abort(): void {
this.activeApi?.abort();
this.activeApi = null;
return createAiApi(aiEnv, provider, this.log);
}
}

View File

@ -196,15 +196,6 @@ export abstract class AiApi {
streamCallback?: IAiResponseStreamFn,
): Promise<IAiChatResponse>;
/**
* Forcefully abort any in-progress API request.
* Provider-specific implementations (e.g., Ollama) should terminate
* the underlying HTTP request immediately. Default is no-op.
*/
abort(): void {
// Override in provider-specific implementations
}
protected assertNonEmptyChatResponse(response: IAiChatResponse): void {
const hasResponse = response.response.trim().length > 0;
const hasThinking = !!response.thinking?.trim();

View File

@ -34,11 +34,6 @@ export class OllamaAiApi extends AiApi {
});
}
/** Forcefully abort any in-progress Ollama API request. */
override abort(): void {
this.client.abort();
}
async listModels(): Promise<IAiModelListResult> {
const response = await this.client.list();
const models = response.models.map((model) => {

View File

@ -35,7 +35,6 @@ export interface IChatSession {
provider: IAiProvider | GadgetId;
selectedModel: string;
reasoningEffort?: ReasoningEffort;
numCtx?: number;
stats: {
turnCount: number;
toolCallCount: number;

View File

@ -86,7 +86,6 @@ export interface IChatTurn {
provider: IAiProvider | GadgetId;
llm: string; // id/name of the model used to process the prompt
reasoningEffort?: ReasoningEffort;
numCtx?: number;
mode: ChatSessionMode;
status: ChatTurnStatus;
prompts: IChatTurnPrompts;

View File

@ -1,27 +0,0 @@
#!/usr/bin/env node
/**
* forever-sleep.js
* A script that sleeps, wakes up every 5 seconds, logs to stdout, then goes back to sleep.
*/
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
console.log('forever-sleep: Starting...');
while (true) {
const timestamp = new Date().toISOString();
console.log(`forever-sleep: ${timestamp} - Waking up (sleeping for 5 seconds)...`);
await sleep(5000);
const timestampAfter = new Date().toISOString();
console.log(`forever-sleep: ${timestampAfter} - Back to sleep...`);
}
}
main().catch(err => {
console.error('forever-sleep: Error:', err);
process.exit(1);
});