Compare commits
5 Commits
96663f7c88
...
633ebf4a78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633ebf4a78 | ||
|
|
140c8a75ee | ||
|
|
8650533697 | ||
|
|
4c628a25a2 | ||
|
|
d990d0dd71 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,4 +3,5 @@ gadget-drone.*.log
|
||||
logfetch
|
||||
|
||||
.gadget
|
||||
gadget/
|
||||
node_modules
|
||||
|
||||
287
CHANGE.md
Normal file
287
CHANGE.md
Normal file
@ -0,0 +1,287 @@
|
||||
# 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
|
||||
@ -16,27 +16,33 @@ or is aborted, the abort signal mechanism is cleaned up end-to-end.
|
||||
|
||||
```
|
||||
[Frontend] --abortWorkOrder(cb)--> [CodeSession] --abortWorkOrder(cb)--> [Drone]
|
||||
|
|
||||
AgentService
|
||||
.abortCurrentWorkOrder()
|
||||
|
|
||||
abortController.abort()
|
||||
|
|
||||
cb(true, "Abort signaled")
|
||||
|
|
||||
|
|
||||
AgentService
|
||||
.abortCurrentWorkOrder()
|
||||
|
|
||||
abortController.abort()
|
||||
|
|
||||
AiService.abort()
|
||||
(calls AI provider's
|
||||
abort() which for
|
||||
Ollama terminates the
|
||||
HTTP request immediately)
|
||||
|
|
||||
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
|
||||
@ -74,6 +80,10 @@ 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 ...`
|
||||
@ -81,6 +91,9 @@ No `SocketEvents` changes needed — the drone listens on its own socket.
|
||||
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`
|
||||
@ -108,7 +121,18 @@ and is detected downstream via `error.name === "AbortError"`.
|
||||
```
|
||||
- `finally` block sets `this.abortController = null`
|
||||
- Public method `abortCurrentWorkOrder(): boolean` — calls
|
||||
`this.abortController?.abort()`, returns `true` if there was a controller
|
||||
`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
|
||||
|
||||
**`gadget-drone/src/gadget-drone.ts`**:
|
||||
|
||||
@ -193,10 +217,11 @@ 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` |
|
||||
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through |
|
||||
| `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/openai.ts` | Same for `generate()`, `readStreamingChatCompletion()`, `readNonStreamingChatCompletion()` |
|
||||
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` |
|
||||
| `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/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` |
|
||||
@ -214,9 +239,16 @@ 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. The
|
||||
AbortError propagates asynchronously through the SDK stream and is caught
|
||||
separately in the agent loop.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
43
gadget-code/frontend/src/components/ProjectPanel.tsx
Normal file
43
gadget-code/frontend/src/components/ProjectPanel.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
298
gadget-code/frontend/src/components/SessionPanel.tsx
Normal file
298
gadget-code/frontend/src/components/SessionPanel.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -358,6 +358,7 @@ export interface ChatSession {
|
||||
provider: string | AiProvider;
|
||||
selectedModel: string;
|
||||
reasoningEffort?: string;
|
||||
numCtx?: number;
|
||||
stats: ChatSessionStats;
|
||||
pins: Array<{ _id?: string; content: string }>;
|
||||
}
|
||||
@ -421,6 +422,7 @@ export interface ChatTurn {
|
||||
session: string | ChatSession;
|
||||
provider: string | AiProvider;
|
||||
llm: string;
|
||||
numCtx?: number;
|
||||
mode: ChatSessionMode;
|
||||
status: "processing" | "finished" | "aborted" | "error";
|
||||
prompts: ChatTurnPrompts;
|
||||
|
||||
@ -3,7 +3,8 @@ 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 WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
|
||||
import SessionPanel from '../components/SessionPanel';
|
||||
import ProjectPanel from '../components/ProjectPanel';
|
||||
import FilesPanel from '../components/FilesPanel';
|
||||
import LogPanel from '../components/LogPanel';
|
||||
import ChatTurnComponent from '../components/ChatTurn';
|
||||
@ -67,6 +68,8 @@ 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('');
|
||||
@ -213,6 +216,15 @@ 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');
|
||||
@ -792,13 +804,16 @@ export default function ChatSessionView() {
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedModelCapabilities = useCallback(() => {
|
||||
const getSelectedModel = useCallback(() => {
|
||||
const provider = providers.find(p => p._id === selectedProviderId);
|
||||
if (!provider) return null;
|
||||
const model = provider.models.find(m => m.id === selectedModelId);
|
||||
return model?.capabilities || null;
|
||||
return provider.models.find(m => m.id === selectedModelId) || null;
|
||||
}, [providers, selectedProviderId, selectedModelId]);
|
||||
|
||||
const getSelectedModelCapabilities = useCallback(() => {
|
||||
return getSelectedModel()?.capabilities || null;
|
||||
}, [getSelectedModel]);
|
||||
|
||||
const handleReasoningChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!session) return;
|
||||
|
||||
@ -820,6 +835,28 @@ 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';
|
||||
};
|
||||
@ -858,6 +895,13 @@ 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) {
|
||||
@ -1164,232 +1208,52 @@ export default function ChatSessionView() {
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col">
|
||||
{/* 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>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<ProjectPanel
|
||||
project={project}
|
||||
onNavigateToProject={() => navigate(`/projects/${project?.slug}`)}
|
||||
/>
|
||||
|
||||
{/* FILES Panel */}
|
||||
<FilesPanel
|
||||
workspaceMode={workspaceMode}
|
||||
onFileSelect={(path) => {
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
providerApi,
|
||||
} from "../lib/api";
|
||||
import { socketClient } from "../lib/socket";
|
||||
import gabAiImage from "./assets/gab-ai.jpeg";
|
||||
|
||||
interface ProjectManagerProps {
|
||||
user: User | null;
|
||||
@ -231,7 +232,11 @@ 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);
|
||||
|
||||
@ -279,7 +284,9 @@ function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps
|
||||
</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>
|
||||
|
||||
@ -652,118 +659,152 @@ 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-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 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>
|
||||
<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 && (
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Model *
|
||||
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={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
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"
|
||||
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 value="">Select a provider</option>
|
||||
{providers.map((provider) => (
|
||||
<option key={provider._id} value={provider._id}>
|
||||
{provider.name} ({provider.apiType})
|
||||
</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>
|
||||
{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 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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
@ -774,7 +815,9 @@ 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);
|
||||
|
||||
@ -839,7 +882,10 @@ 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);
|
||||
|
||||
BIN
gadget-code/frontend/src/pages/assets/gab-ai.jpeg
Normal file
BIN
gadget-code/frontend/src/pages/assets/gab-ai.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
29
gadget-code/frontend/src/types/assets.d.ts
vendored
Normal file
29
gadget-code/frontend/src/types/assets.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
@ -179,6 +179,7 @@ class ChatSessionController extends DtpController {
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: "off" | "low" | "medium" | "high";
|
||||
numCtx: number;
|
||||
}> = {};
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
@ -204,6 +205,12 @@ 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,
|
||||
|
||||
@ -30,6 +30,7 @@ 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 },
|
||||
|
||||
@ -69,6 +69,7 @@ export const ChatTurnSchema = new Schema<IChatTurn>({
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
},
|
||||
numCtx: { type: Number },
|
||||
mode: {
|
||||
type: String,
|
||||
enum: ChatSessionMode,
|
||||
|
||||
@ -202,6 +202,7 @@ class ChatSessionService extends DtpService {
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: ReasoningEffort;
|
||||
numCtx: number;
|
||||
}>,
|
||||
): Promise<IChatSession> {
|
||||
const session = await ChatSession.findById(chatSessionId);
|
||||
@ -230,6 +231,9 @@ class ChatSessionService extends DtpService {
|
||||
if (updates.reasoningEffort !== undefined) {
|
||||
session.reasoningEffort = updates.reasoningEffort;
|
||||
}
|
||||
if (updates.numCtx !== undefined) {
|
||||
session.numCtx = updates.numCtx;
|
||||
}
|
||||
|
||||
await session.save();
|
||||
|
||||
@ -276,6 +280,7 @@ 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: {
|
||||
|
||||
@ -218,7 +218,7 @@ class AgentService extends GadgetService {
|
||||
try {
|
||||
response = await AiService.chat(
|
||||
turn.provider,
|
||||
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning),
|
||||
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning, turn.numCtx),
|
||||
chatOptions,
|
||||
this.makeStreamHandler(socket),
|
||||
);
|
||||
@ -337,6 +337,7 @@ class AgentService extends GadgetService {
|
||||
abortCurrentWorkOrder(): boolean {
|
||||
if (!this.abortController) return false;
|
||||
this.abortController.abort();
|
||||
AiService.abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -473,6 +474,7 @@ 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 {
|
||||
@ -483,7 +485,7 @@ class AgentService extends GadgetService {
|
||||
topP: settings?.topP ?? 0.9,
|
||||
topK: settings?.topK ?? 40,
|
||||
numPredict: settings?.numPredict ?? -1, // -1 = unlimited (Ollama)
|
||||
numCtx: settings?.numCtx ?? 131072, // 128k fallback
|
||||
numCtx: numCtxOverride ?? settings?.numCtx ?? 131072, // session override > model settings > 128k fallback
|
||||
maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback
|
||||
},
|
||||
};
|
||||
@ -625,7 +627,7 @@ class AgentService extends GadgetService {
|
||||
|
||||
const response = await AiService.chat(
|
||||
provider,
|
||||
this.buildDroneModelConfig(provider, turn.llm, false),
|
||||
this.buildDroneModelConfig(provider, turn.llm, false, turn.numCtx),
|
||||
chatOptions,
|
||||
streamHandler,
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ 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,
|
||||
@ -47,6 +48,8 @@ class AiService extends GadgetService {
|
||||
return "svc:ai";
|
||||
}
|
||||
|
||||
private activeApi: AiApi | null = null;
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.log.info("started");
|
||||
}
|
||||
@ -103,7 +106,11 @@ class AiService extends GadgetService {
|
||||
});
|
||||
const api = this.getApi(config);
|
||||
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||
return api.generate(modelConfig, options, streamCallback);
|
||||
try {
|
||||
return await api.generate(modelConfig, options, streamCallback);
|
||||
} finally {
|
||||
this.activeApi = null;
|
||||
}
|
||||
}
|
||||
|
||||
async chat(
|
||||
@ -121,11 +128,23 @@ class AiService extends GadgetService {
|
||||
});
|
||||
const api = this.getApi(config);
|
||||
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||
return await api.chat(modelConfig, options, streamCallback);
|
||||
try {
|
||||
return await api.chat(modelConfig, options, streamCallback);
|
||||
} finally {
|
||||
this.activeApi = null;
|
||||
}
|
||||
}
|
||||
|
||||
getApi(provider: AiProviderConfig) {
|
||||
return createAiApi(aiEnv, provider, this.log);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -196,6 +196,15 @@ 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();
|
||||
|
||||
@ -34,6 +34,11 @@ 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) => {
|
||||
|
||||
@ -35,6 +35,7 @@ export interface IChatSession {
|
||||
provider: IAiProvider | GadgetId;
|
||||
selectedModel: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
numCtx?: number;
|
||||
stats: {
|
||||
turnCount: number;
|
||||
toolCallCount: number;
|
||||
|
||||
@ -86,6 +86,7 @@ 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;
|
||||
|
||||
27
scripts/forever-sleep.js
Normal file
27
scripts/forever-sleep.js
Normal file
@ -0,0 +1,27 @@
|
||||
#!/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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user