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
|
logfetch
|
||||||
|
|
||||||
.gadget
|
.gadget
|
||||||
|
gadget/
|
||||||
node_modules
|
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]
|
[Frontend] --abortWorkOrder(cb)--> [CodeSession] --abortWorkOrder(cb)--> [Drone]
|
||||||
|
|
|
|
||||||
AgentService
|
AgentService
|
||||||
.abortCurrentWorkOrder()
|
.abortCurrentWorkOrder()
|
||||||
|
|
|
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
|
|
|
|
||||||
cb(true, "Abort signaled")
|
AiService.abort()
|
||||||
|
|
(calls AI provider's
|
||||||
|
abort() which for
|
||||||
|
Ollama terminates the
|
||||||
|
HTTP request immediately)
|
||||||
|
|
|
||||||
|
cb(true, "Abort signaled")
|
||||||
|
|
|
||||||
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
||||||
|
|
|
|
||||||
(AbortError thrown
|
(AbortError thrown
|
||||||
in AI provider)
|
in AI provider)
|
||||||
|
|
|
|
||||||
AgentService catches
|
AgentService catches
|
||||||
AbortError, emits
|
AbortError, emits
|
||||||
workOrderComplete(turnId, true, "aborted")
|
workOrderComplete(turnId, true, "aborted")
|
||||||
|
|
|
|
||||||
[DroneSession] --workOrderComplete(turnId, true, "aborted")--> [CodeSession]
|
[DroneSession] --workOrderComplete(turnId, true, "aborted")--> [CodeSession]
|
||||||
| |
|
| |
|
||||||
turn.status = turn.status = "aborted"
|
turn.status = turn.status = "aborted"
|
||||||
ChatTurnStatus.Aborted displayed subtly
|
ChatTurnStatus.Aborted displayed subtly
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key design decision**: `workOrderComplete` is used with `success=true` and
|
**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`
|
- `IAiChatOptions`
|
||||||
- `IAiGenerateOptions`
|
- `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()`:
|
**`packages/ai/src/ollama.ts`** — In both `generate()` and `chat()`:
|
||||||
|
|
||||||
1. Pre-request check: `if (options.signal?.aborted) throw ...`
|
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:
|
3. Per-chunk check at top of `for await` loop:
|
||||||
`if (options.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError")`
|
`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:
|
**`packages/ai/src/openai.ts`** — Same pattern in:
|
||||||
|
|
||||||
- `generate()` — pre-check, pass `signal` to SDK, per-chunk in `for await`
|
- `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`
|
- `finally` block sets `this.abortController = null`
|
||||||
- Public method `abortCurrentWorkOrder(): boolean` — calls
|
- 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`**:
|
**`gadget-drone/src/gadget-drone.ts`**:
|
||||||
|
|
||||||
@ -193,10 +217,11 @@ status: "processing" | "finished" | "aborted" | "error";
|
|||||||
| `.gitignore` | Add root-level log file patterns |
|
| `.gitignore` | Add root-level log file patterns |
|
||||||
| `packages/api/src/messages/ide.ts` | New `AbortWorkOrderCallback`, `AbortWorkOrderMessage` |
|
| `packages/api/src/messages/ide.ts` | New `AbortWorkOrderCallback`, `AbortWorkOrderMessage` |
|
||||||
| `packages/api/src/messages/socket.ts` | Register `abortWorkOrder` in both event interfaces |
|
| `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/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 |
|
| `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()` |
|
| `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-drone/src/gadget-drone.ts` | `onAbortWorkOrder` socket handler |
|
||||||
| `gadget-code/src/lib/code-session.ts` | Forward `abortWorkOrder` to drone |
|
| `gadget-code/src/lib/code-session.ts` | Forward `abortWorkOrder` to drone |
|
||||||
| `gadget-code/src/lib/drone-session.ts` | Detect `"aborted"` in `workOrderComplete` |
|
| `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.
|
event or change the `workOrderComplete` signature.
|
||||||
- The `AbortController` is created per-process() call and cleaned up in
|
- The `AbortController` is created per-process() call and cleaned up in
|
||||||
`finally`. It is never shared across work orders.
|
`finally`. It is never shared across work orders.
|
||||||
- The abort callback returns immediately after the controller is signaled. The
|
- The abort callback returns immediately after the controller is signaled and
|
||||||
AbortError propagates asynchronously through the SDK stream and is caught
|
`AiService.abort()` is called. The AbortError propagates asynchronously
|
||||||
separately in the agent loop.
|
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`)
|
- Subagent loops also receive the abort signal (via `this.abortController?.signal`)
|
||||||
and abort naturally — no special subagent abort handling needed.
|
and abort naturally — no special subagent abort handling needed.
|
||||||
- The Esc handler is a global `window` keydown listener. It is registered ONLY
|
- 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;
|
provider: string | AiProvider;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
reasoningEffort?: string;
|
reasoningEffort?: string;
|
||||||
|
numCtx?: number;
|
||||||
stats: ChatSessionStats;
|
stats: ChatSessionStats;
|
||||||
pins: Array<{ _id?: string; content: string }>;
|
pins: Array<{ _id?: string; content: string }>;
|
||||||
}
|
}
|
||||||
@ -421,6 +422,7 @@ export interface ChatTurn {
|
|||||||
session: string | ChatSession;
|
session: string | ChatSession;
|
||||||
provider: string | AiProvider;
|
provider: string | AiProvider;
|
||||||
llm: string;
|
llm: string;
|
||||||
|
numCtx?: number;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
status: "processing" | "finished" | "aborted" | "error";
|
status: "processing" | "finished" | "aborted" | "error";
|
||||||
prompts: ChatTurnPrompts;
|
prompts: ChatTurnPrompts;
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
import { socketClient } from '../lib/socket';
|
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 { chatSessionApi, projectApi, providerApi, type ChatSession, type ChatTurn, type ChatTurnBlock, ChatTurnStats, ChatSessionMode, type AiProvider, type Project } from '../lib/api';
|
||||||
import { WorkspaceMode } from '../lib/types';
|
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 FilesPanel from '../components/FilesPanel';
|
||||||
import LogPanel from '../components/LogPanel';
|
import LogPanel from '../components/LogPanel';
|
||||||
import ChatTurnComponent from '../components/ChatTurn';
|
import ChatTurnComponent from '../components/ChatTurn';
|
||||||
@ -67,6 +68,8 @@ export default function ChatSessionView() {
|
|||||||
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
|
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||||||
const [sessionReasoningEffort, setSessionReasoningEffort] = useState<string>('off');
|
const [sessionReasoningEffort, setSessionReasoningEffort] = useState<string>('off');
|
||||||
|
const [sessionNumCtx, setSessionNumCtx] = useState<number>(131072);
|
||||||
|
const [isUpdatingNumCtx, setIsUpdatingNumCtx] = useState(false);
|
||||||
const [isEditingProvider, setIsEditingProvider] = useState(false);
|
const [isEditingProvider, setIsEditingProvider] = useState(false);
|
||||||
const [editProviderId, setEditProviderId] = useState('');
|
const [editProviderId, setEditProviderId] = useState('');
|
||||||
const [editModelId, setEditModelId] = useState('');
|
const [editModelId, setEditModelId] = useState('');
|
||||||
@ -213,6 +216,15 @@ export default function ChatSessionView() {
|
|||||||
setSelectedProviderId(providerId || '');
|
setSelectedProviderId(providerId || '');
|
||||||
setSelectedModelId(sessionData.selectedModel || '');
|
setSelectedModelId(sessionData.selectedModel || '');
|
||||||
setSessionReasoningEffort(sessionData.reasoningEffort || 'off');
|
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
|
// Set connection state to connecting - will update on socket connect
|
||||||
setConnectionState('connecting');
|
setConnectionState('connecting');
|
||||||
@ -792,13 +804,16 @@ export default function ChatSessionView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedModelCapabilities = useCallback(() => {
|
const getSelectedModel = useCallback(() => {
|
||||||
const provider = providers.find(p => p._id === selectedProviderId);
|
const provider = providers.find(p => p._id === selectedProviderId);
|
||||||
if (!provider) return null;
|
if (!provider) return null;
|
||||||
const model = provider.models.find(m => m.id === selectedModelId);
|
return provider.models.find(m => m.id === selectedModelId) || null;
|
||||||
return model?.capabilities || null;
|
|
||||||
}, [providers, selectedProviderId, selectedModelId]);
|
}, [providers, selectedProviderId, selectedModelId]);
|
||||||
|
|
||||||
|
const getSelectedModelCapabilities = useCallback(() => {
|
||||||
|
return getSelectedModel()?.capabilities || null;
|
||||||
|
}, [getSelectedModel]);
|
||||||
|
|
||||||
const handleReasoningChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleReasoningChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
if (!session) return;
|
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) => {
|
const getProviderName = (id: string) => {
|
||||||
return providers.find(p => p._id === id)?.name ?? 'None';
|
return providers.find(p => p._id === id)?.name ?? 'None';
|
||||||
};
|
};
|
||||||
@ -858,6 +895,13 @@ export default function ChatSessionView() {
|
|||||||
setSession(updatedSession);
|
setSession(updatedSession);
|
||||||
setSelectedProviderId(editProviderId);
|
setSelectedProviderId(editProviderId);
|
||||||
setSelectedModelId(editModelId);
|
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);
|
setIsEditingProvider(false);
|
||||||
showToast('Provider and model updated');
|
showToast('Provider and model updated');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1164,232 +1208,52 @@ export default function ChatSessionView() {
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col">
|
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col">
|
||||||
{/* SESSION Panel */}
|
<SessionPanel
|
||||||
<div className="border-b border-border-subtle shrink-0">
|
session={session}
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
workspaceMode={workspaceMode}
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
sessionLocked={sessionLocked}
|
||||||
Session
|
providers={providers}
|
||||||
</h3>
|
selectedProviderId={selectedProviderId}
|
||||||
<WorkspaceModeIndicator
|
selectedModelId={selectedModelId}
|
||||||
mode={workspaceMode}
|
sessionReasoningEffort={sessionReasoningEffort}
|
||||||
onChange={handleWorkspaceModeChange}
|
sessionNumCtx={sessionNumCtx}
|
||||||
disabled={!sessionLocked || isEditingProvider}
|
isEditingProvider={isEditingProvider}
|
||||||
/>
|
editProviderId={editProviderId}
|
||||||
</div>
|
editModelId={editModelId}
|
||||||
<div className="p-4 space-y-3">
|
isUpdatingProvider={isUpdatingProvider}
|
||||||
<div>
|
isEditingName={isEditingName}
|
||||||
<div className="text-xs text-text-muted">Name</div>
|
editName={editName}
|
||||||
{isEditingName ? (
|
isUpdatingName={isUpdatingName}
|
||||||
<div className="flex gap-2 mt-1">
|
isUpdatingMode={isUpdatingMode}
|
||||||
<input
|
isUpdatingReasoning={isUpdatingReasoning}
|
||||||
type="text"
|
isUpdatingNumCtx={isUpdatingNumCtx}
|
||||||
value={editName}
|
onWorkspaceModeChange={handleWorkspaceModeChange}
|
||||||
onChange={(e) => setEditName(e.target.value)}
|
onModeChange={handleModeChange}
|
||||||
onKeyDown={(e) => {
|
onReasoningChange={handleReasoningChange}
|
||||||
if (e.key === 'Enter') { e.preventDefault(); handleSaveName(); }
|
onNumCtxChange={handleNumCtxChange}
|
||||||
if (e.key === 'Escape') { e.preventDefault(); handleCancelEditName(); }
|
onNumCtxCommit={handleNumCtxCommit}
|
||||||
}}
|
onStartEditProvider={handleStartEditProvider}
|
||||||
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"
|
onSaveProvider={handleSaveProvider}
|
||||||
autoFocus
|
onCancelEditProvider={handleCancelEditProvider}
|
||||||
/>
|
onEditProviderIdChange={setEditProviderId}
|
||||||
<button
|
onEditModelIdChange={setEditModelId}
|
||||||
title="Save"
|
onStartEditName={handleStartEditName}
|
||||||
onClick={handleSaveName}
|
onSaveName={handleSaveName}
|
||||||
disabled={!editName.trim() || isUpdatingName}
|
onCancelEditName={handleCancelEditName}
|
||||||
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors self-center"
|
onEditNameChange={setEditName}
|
||||||
>
|
getProviderName={getProviderName}
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
getModelName={getModelName}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
getSortedModels={getSortedModels}
|
||||||
</svg>
|
truncateModelName={truncateModelName}
|
||||||
</button>
|
getSelectedModelCapabilities={getSelectedModelCapabilities}
|
||||||
<button
|
getSelectedModel={getSelectedModel}
|
||||||
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>
|
|
||||||
|
|
||||||
{/* PROJECT Panel */}
|
<ProjectPanel
|
||||||
<div className="border-b border-border-subtle shrink-0">
|
project={project}
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
onNavigateToProject={() => navigate(`/projects/${project?.slug}`)}
|
||||||
<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
|
<FilesPanel
|
||||||
workspaceMode={workspaceMode}
|
workspaceMode={workspaceMode}
|
||||||
onFileSelect={(path) => {
|
onFileSelect={(path) => {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
providerApi,
|
providerApi,
|
||||||
} from "../lib/api";
|
} from "../lib/api";
|
||||||
import { socketClient } from "../lib/socket";
|
import { socketClient } from "../lib/socket";
|
||||||
|
import gabAiImage from "./assets/gab-ai.jpeg";
|
||||||
|
|
||||||
interface ProjectManagerProps {
|
interface ProjectManagerProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
@ -231,7 +232,11 @@ interface ProjectInspectorProps {
|
|||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
|
function ProjectInspector({
|
||||||
|
project,
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
}: ProjectInspectorProps) {
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
@ -279,7 +284,9 @@ function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-bg-secondary border border-border-default rounded">
|
<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="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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -652,118 +659,152 @@ function NewChatSessionModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const affiliateHref = "https://gab.ai/?ref=c7182f6e";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
<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">
|
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-2xl flex gap-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
|
<div className="w-48 flex-shrink-0 flex flex-col">
|
||||||
{selectedDrone && (
|
<a
|
||||||
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
|
href={affiliateHref}
|
||||||
<div className="text-xs text-text-muted mb-1">Selected Drone</div>
|
target="_blank"
|
||||||
<div className="font-mono text-sm text-text-primary">
|
rel="noopener noreferrer"
|
||||||
{selectedDrone.hostname}
|
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>
|
||||||
<div className="text-xs text-text-muted truncate">
|
)}
|
||||||
{selectedDrone.workspaceDir}
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
</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>
|
<div>
|
||||||
<label className="block text-sm text-text-secondary mb-1">
|
<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>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedProviderId}
|
||||||
onChange={(e) => setSelectedModel(e.target.value)}
|
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"
|
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>
|
<option value="">Select a provider</option>
|
||||||
{[...selectedProvider.models].sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
|
{providers.map((provider) => (
|
||||||
<option key={model.id} value={model.id}>
|
<option key={provider._id} value={provider._id}>
|
||||||
{model.name}{" "}
|
{provider.name} ({provider.apiType})
|
||||||
{model.parameterLabel ? `(${model.parameterLabel})` : ""}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
{selectedProvider && (
|
||||||
<label className="block text-sm text-text-secondary mb-1">
|
<div>
|
||||||
Mode
|
<label className="block text-sm text-text-secondary mb-1">
|
||||||
</label>
|
Model *
|
||||||
<select
|
</label>
|
||||||
value={mode}
|
<select
|
||||||
onChange={(e) => setMode(e.target.value)}
|
value={selectedModel}
|
||||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
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"
|
||||||
<option value="plan">Plan - Planning and brainstorming</option>
|
disabled={selectedProvider.models.length === 0}
|
||||||
<option value="build">Build - Building and coding</option>
|
>
|
||||||
<option value="test">Test - Testing and debugging</option>
|
<option value="">Select a model</option>
|
||||||
<option value="ship">Ship - Finalizing and shipping</option>
|
{[...selectedProvider.models]
|
||||||
<option value="dev">Dev - Working on Gadget Code itself</option>
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
</select>
|
.map((model) => (
|
||||||
</div>
|
<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">
|
<div>
|
||||||
<button
|
<label className="block text-sm text-text-secondary mb-1">
|
||||||
type="submit"
|
Mode
|
||||||
disabled={creating || !selectedProviderId || !selectedModel}
|
</label>
|
||||||
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 flex-1"
|
<select
|
||||||
>
|
value={mode}
|
||||||
{creating ? "Creating..." : "Create Session"}
|
onChange={(e) => setMode(e.target.value)}
|
||||||
</button>
|
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||||
<button
|
>
|
||||||
type="button"
|
<option value="plan">Plan - Planning and brainstorming</option>
|
||||||
onClick={onCancel}
|
<option value="build">Build - Building and coding</option>
|
||||||
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
|
<option value="test">Test - Testing and debugging</option>
|
||||||
>
|
<option value="ship">Ship - Finalizing and shipping</option>
|
||||||
Cancel
|
<option value="dev">Dev - Working on Gadget Code itself</option>
|
||||||
</button>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -774,7 +815,9 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
|||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [showNewForm, setShowNewForm] = useState(false);
|
const [showNewForm, setShowNewForm] = useState(false);
|
||||||
|
|
||||||
@ -839,7 +882,10 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
|||||||
console.error("Failed to lock drone session");
|
console.error("Failed to lock drone session");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem('dtp_drone_registration', JSON.stringify(selectedDrone));
|
localStorage.setItem(
|
||||||
|
"dtp_drone_registration",
|
||||||
|
JSON.stringify(selectedDrone),
|
||||||
|
);
|
||||||
navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`);
|
navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to open chat session", 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;
|
selectedModel: string;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
reasoningEffort: "off" | "low" | "medium" | "high";
|
reasoningEffort: "off" | "low" | "medium" | "high";
|
||||||
|
numCtx: number;
|
||||||
}> = {};
|
}> = {};
|
||||||
|
|
||||||
if (updates.name !== undefined) {
|
if (updates.name !== undefined) {
|
||||||
@ -204,6 +205,12 @@ class ChatSessionController extends DtpController {
|
|||||||
}
|
}
|
||||||
allowedUpdates.reasoningEffort = updates.reasoningEffort;
|
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(
|
const session = await ChatSessionService.update(
|
||||||
res.locals.chatSession._id,
|
res.locals.chatSession._id,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export const ChatSessionSchema = new Schema<IChatSession>({
|
|||||||
enum: ["off", "low", "medium", "high"],
|
enum: ["off", "low", "medium", "high"],
|
||||||
default: "off",
|
default: "off",
|
||||||
},
|
},
|
||||||
|
numCtx: { type: Number },
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: { type: Number, default: 0, required: true },
|
turnCount: { type: Number, default: 0, required: true },
|
||||||
toolCallCount: { 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"],
|
enum: ["off", "low", "medium", "high"],
|
||||||
default: "off",
|
default: "off",
|
||||||
},
|
},
|
||||||
|
numCtx: { type: Number },
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ChatSessionMode,
|
enum: ChatSessionMode,
|
||||||
|
|||||||
@ -202,6 +202,7 @@ class ChatSessionService extends DtpService {
|
|||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
reasoningEffort: ReasoningEffort;
|
reasoningEffort: ReasoningEffort;
|
||||||
|
numCtx: number;
|
||||||
}>,
|
}>,
|
||||||
): Promise<IChatSession> {
|
): Promise<IChatSession> {
|
||||||
const session = await ChatSession.findById(chatSessionId);
|
const session = await ChatSession.findById(chatSessionId);
|
||||||
@ -230,6 +231,9 @@ class ChatSessionService extends DtpService {
|
|||||||
if (updates.reasoningEffort !== undefined) {
|
if (updates.reasoningEffort !== undefined) {
|
||||||
session.reasoningEffort = updates.reasoningEffort;
|
session.reasoningEffort = updates.reasoningEffort;
|
||||||
}
|
}
|
||||||
|
if (updates.numCtx !== undefined) {
|
||||||
|
session.numCtx = updates.numCtx;
|
||||||
|
}
|
||||||
|
|
||||||
await session.save();
|
await session.save();
|
||||||
|
|
||||||
@ -276,6 +280,7 @@ class ChatSessionService extends DtpService {
|
|||||||
provider: session.provider,
|
provider: session.provider,
|
||||||
llm: session.selectedModel,
|
llm: session.selectedModel,
|
||||||
reasoningEffort: (session as IChatSession).reasoningEffort || "off",
|
reasoningEffort: (session as IChatSession).reasoningEffort || "off",
|
||||||
|
numCtx: (session as IChatSession).numCtx,
|
||||||
mode: session.mode,
|
mode: session.mode,
|
||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
prompts: {
|
prompts: {
|
||||||
|
|||||||
@ -218,7 +218,7 @@ class AgentService extends GadgetService {
|
|||||||
try {
|
try {
|
||||||
response = await AiService.chat(
|
response = await AiService.chat(
|
||||||
turn.provider,
|
turn.provider,
|
||||||
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning),
|
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning, turn.numCtx),
|
||||||
chatOptions,
|
chatOptions,
|
||||||
this.makeStreamHandler(socket),
|
this.makeStreamHandler(socket),
|
||||||
);
|
);
|
||||||
@ -337,6 +337,7 @@ class AgentService extends GadgetService {
|
|||||||
abortCurrentWorkOrder(): boolean {
|
abortCurrentWorkOrder(): boolean {
|
||||||
if (!this.abortController) return false;
|
if (!this.abortController) return false;
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
|
AiService.abort();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,6 +474,7 @@ class AgentService extends GadgetService {
|
|||||||
provider: IAiProvider | GadgetId,
|
provider: IAiProvider | GadgetId,
|
||||||
modelId: string,
|
modelId: string,
|
||||||
reasoning: boolean | "low" | "medium" | "high",
|
reasoning: boolean | "low" | "medium" | "high",
|
||||||
|
numCtxOverride?: number,
|
||||||
): Omit<IDroneModelConfig, "provider"> {
|
): Omit<IDroneModelConfig, "provider"> {
|
||||||
const settings = this.getModelSettings(provider, modelId);
|
const settings = this.getModelSettings(provider, modelId);
|
||||||
return {
|
return {
|
||||||
@ -483,7 +485,7 @@ class AgentService extends GadgetService {
|
|||||||
topP: settings?.topP ?? 0.9,
|
topP: settings?.topP ?? 0.9,
|
||||||
topK: settings?.topK ?? 40,
|
topK: settings?.topK ?? 40,
|
||||||
numPredict: settings?.numPredict ?? -1, // -1 = unlimited (Ollama)
|
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
|
maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -625,7 +627,7 @@ class AgentService extends GadgetService {
|
|||||||
|
|
||||||
const response = await AiService.chat(
|
const response = await AiService.chat(
|
||||||
provider,
|
provider,
|
||||||
this.buildDroneModelConfig(provider, turn.llm, false),
|
this.buildDroneModelConfig(provider, turn.llm, false, turn.numCtx),
|
||||||
chatOptions,
|
chatOptions,
|
||||||
streamHandler,
|
streamHandler,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const aiEnv: IAiEnvironment = {
|
|||||||
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
|
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
|
||||||
import { GadgetService } from "../lib/service.js";
|
import { GadgetService } from "../lib/service.js";
|
||||||
import {
|
import {
|
||||||
|
AiApi,
|
||||||
type IAiChatOptions,
|
type IAiChatOptions,
|
||||||
type IAiChatResponse,
|
type IAiChatResponse,
|
||||||
type IAiGenerateOptions,
|
type IAiGenerateOptions,
|
||||||
@ -47,6 +48,8 @@ class AiService extends GadgetService {
|
|||||||
return "svc:ai";
|
return "svc:ai";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private activeApi: AiApi | null = null;
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.log.info("started");
|
this.log.info("started");
|
||||||
}
|
}
|
||||||
@ -103,7 +106,11 @@ class AiService extends GadgetService {
|
|||||||
});
|
});
|
||||||
const api = this.getApi(config);
|
const api = this.getApi(config);
|
||||||
const modelConfig: IAiModelConfig = { ...model, provider: 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(
|
async chat(
|
||||||
@ -121,11 +128,23 @@ class AiService extends GadgetService {
|
|||||||
});
|
});
|
||||||
const api = this.getApi(config);
|
const api = this.getApi(config);
|
||||||
const modelConfig: IAiModelConfig = { ...model, provider: 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) {
|
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,
|
streamCallback?: IAiResponseStreamFn,
|
||||||
): Promise<IAiChatResponse>;
|
): 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 {
|
protected assertNonEmptyChatResponse(response: IAiChatResponse): void {
|
||||||
const hasResponse = response.response.trim().length > 0;
|
const hasResponse = response.response.trim().length > 0;
|
||||||
const hasThinking = !!response.thinking?.trim();
|
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> {
|
async listModels(): Promise<IAiModelListResult> {
|
||||||
const response = await this.client.list();
|
const response = await this.client.list();
|
||||||
const models = response.models.map((model) => {
|
const models = response.models.map((model) => {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export interface IChatSession {
|
|||||||
provider: IAiProvider | GadgetId;
|
provider: IAiProvider | GadgetId;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
reasoningEffort?: ReasoningEffort;
|
reasoningEffort?: ReasoningEffort;
|
||||||
|
numCtx?: number;
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: number;
|
turnCount: number;
|
||||||
toolCallCount: number;
|
toolCallCount: number;
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export interface IChatTurn {
|
|||||||
provider: IAiProvider | GadgetId;
|
provider: IAiProvider | GadgetId;
|
||||||
llm: string; // id/name of the model used to process the prompt
|
llm: string; // id/name of the model used to process the prompt
|
||||||
reasoningEffort?: ReasoningEffort;
|
reasoningEffort?: ReasoningEffort;
|
||||||
|
numCtx?: number;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
status: ChatTurnStatus;
|
status: ChatTurnStatus;
|
||||||
prompts: IChatTurnPrompts;
|
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