Compare commits
No commits in common. "633ebf4a784f00f28e86ff0c52cdc4b23f6f89a5" and "96663f7c8854fafb44f2fb826ed9a9269f072bbe" have entirely different histories.
633ebf4a78
...
96663f7c88
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,5 +3,4 @@ gadget-drone.*.log
|
|||||||
logfetch
|
logfetch
|
||||||
|
|
||||||
.gadget
|
.gadget
|
||||||
gadget/
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
287
CHANGE.md
287
CHANGE.md
@ -1,287 +0,0 @@
|
|||||||
# CHANGELOG
|
|
||||||
|
|
||||||
This document tracks all significant changes to Gadget Code, organized by date.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-15 (Thursday)
|
|
||||||
|
|
||||||
**Context Window Configuration Added**
|
|
||||||
|
|
||||||
The most recent change introduces the `numCtx` (Context Window) setting to the SESSION panel, allowing users to configure the context window size for their AI model sessions. This setting is part of a broader enhancement to the model configuration pipeline that also includes `numPredict` and `maxCompletionTokens` parameters.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Added `numCtx` setting to SESSION panel
|
|
||||||
- Updated model configuration pipeline to include `numPredict`, `numCtx`, and `maxCompletionTokens`
|
|
||||||
- Modified ChatSessionView to handle new context window settings
|
|
||||||
- Updated session initialization logic to apply context window configuration
|
|
||||||
- Added Gab AI affiliate link
|
|
||||||
- Changed Ollama abort procedure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-14 (Wednesday)
|
|
||||||
|
|
||||||
**SubProcess Observability Implementation**
|
|
||||||
|
|
||||||
This week focused on implementing comprehensive observability for SubProcess operations within the DroneManager and DroneInspector components, including detailed logging, status tracking, and event emission capabilities.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented Phase 2: SubProcess observability in DroneManager and DroneInspector
|
|
||||||
- Added subprocess status tracking and event emission
|
|
||||||
- Updated UI to display subprocess execution details
|
|
||||||
- Fixed mode change log message formatting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-13 (Tuesday)
|
|
||||||
|
|
||||||
**Documentation and Editor Improvements**
|
|
||||||
|
|
||||||
Documentation updates include adding continuation prompts and remaining steps for User Mode, session summary documentation for MVP completion, and formatting cleanup in AGENTS.md. The editor improvements address a critical issue with CodeMirror editor height constraints in flex layouts.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Added continuation prompt and remaining steps for User Mode documentation
|
|
||||||
- Added session summary for User Mode MVP completion
|
|
||||||
- Fixed CodeMirror editor height constraint in flex layout
|
|
||||||
- Minor formatting cleanup in AGENTS.md
|
|
||||||
- Created new files and prep work for landing page project
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-12 (Monday)
|
|
||||||
|
|
||||||
**FILES Panel and Editor Integration**
|
|
||||||
|
|
||||||
This week marked a significant milestone with the implementation of the FILES panel foundation featuring lazy-loading file tree capabilities. The implementation replaced react-ace with @uiw/react-codemirror to resolve React 19 compatibility issues and Vite dev server problems.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented FILES panel foundation with lazy-loading file tree
|
|
||||||
- Replaced react-ace with @uiw/react-codemirror
|
|
||||||
- Fixed flexbox scrolling for FILES panel sidebar
|
|
||||||
- Added shrink-0 to SESSION and PROJECT panels for proper flex layout
|
|
||||||
- Fixed file tree scrolling and text selection issues
|
|
||||||
- Fixed file tree backend recursion breaking lazy loading
|
|
||||||
- Fixed file tree showing entire workspace instead of project directory
|
|
||||||
- Fixed file tree duplication and corruption on expand
|
|
||||||
- Fixed file tree expand/collapse not rendering children
|
|
||||||
- Added navigation to project in Project Manager using slug
|
|
||||||
- Fixed ACE editor integration crash — React 19 compat, Vite ?url pattern, Error Boundary
|
|
||||||
- Fixed file tree duplication and corruption on expand
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-11 (Sunday)
|
|
||||||
|
|
||||||
**Subagent Processing and Authentication Fixes**
|
|
||||||
|
|
||||||
This weekend's work focused on subagent processing improvements and authentication fixes, including fixes for subagent response handling, session data management, and adding the persona field to sign-in responses.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Fixed subagent response handling and session data
|
|
||||||
- Added persona field to sign-in response
|
|
||||||
- Fixed JWT expiration bugs and drone heartbeat timeouts
|
|
||||||
- Added installation guides, config examples, and systemd service files
|
|
||||||
- Fixed subagent markdown output whitespace using .subagent-compact.gadget-markdown
|
|
||||||
- Applied CSS and style fixes for subagent display
|
|
||||||
- Updated build prompt
|
|
||||||
- Fixed auth: include persona field in sign-in response
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-10 (Saturday)
|
|
||||||
|
|
||||||
**Workspace and Project Philosophy Shift**
|
|
||||||
|
|
||||||
This weekend involved a philosophical shift in how Workspaces and Projects are conceptualized and implemented, with improved separation of concerns and clearer boundaries between the two concepts.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Applied philosophy shifts for Workspaces and Projects
|
|
||||||
- Updated workspace and project conceptual model
|
|
||||||
- Improved separation of concerns between workspace and project
|
|
||||||
- Chat interface fixes and cleanup
|
|
||||||
- Credential provider fix
|
|
||||||
- Ensure apiKey is presented to required consumers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-09 (Friday)
|
|
||||||
|
|
||||||
**Agentic Workflow Loop Rebuild**
|
|
||||||
|
|
||||||
This week involved a complete rebuild of the Agentic Workflow Loop to improve reliability and performance, including better error handling, improved checkpointing, and enhanced event emission capabilities.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Re-built Agentic Workflow Loop with improved reliability
|
|
||||||
- Enhanced streaming response handling
|
|
||||||
- Implemented chat session auto-naming with IDE update
|
|
||||||
- Improved checkpointing and event emission
|
|
||||||
- GadgetLogTransportSocket and the drone-to-IDE log
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-08 (Thursday)
|
|
||||||
|
|
||||||
**Chat Session Heartbeat and User Settings**
|
|
||||||
|
|
||||||
This week focused on implementing chat session heartbeat functionality and user settings management, ensuring that active sessions are properly tracked and can be recovered in case of unexpected disconnections.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented chat session heartbeat and session unlock
|
|
||||||
- Added user settings management
|
|
||||||
- Made reasoning effort configurable
|
|
||||||
- Added editable session name with cog icon
|
|
||||||
- Redesigned sign-in form and presentation
|
|
||||||
- Integrated persona field in User
|
|
||||||
- Reworked provider/model selection with save/cancel pattern
|
|
||||||
- Added navigation to return to Project Manager
|
|
||||||
- Quick fix for when logging.file.enabled is false
|
|
||||||
- Refactor: unify logging into @gadget/api as GadgetLog
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-07 (Wednesday)
|
|
||||||
|
|
||||||
**Streaming Responses Implementation**
|
|
||||||
|
|
||||||
This week saw the implementation of streaming responses for AI model interactions, including proper handling of streaming data from Ollama and other providers with support for real-time token delivery and response completion tracking.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented streaming responses for Ollama
|
|
||||||
- Added streaming response handling and correctness improvements
|
|
||||||
- Created documentation for streaming responses
|
|
||||||
- Improved real-time token delivery and completion tracking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-06 (Tuesday)
|
|
||||||
|
|
||||||
**Agent Tool and Toolbox Refactoring**
|
|
||||||
|
|
||||||
This week involved significant refactoring of the agent tool and toolbox architecture, including unifying logging into @gadget/api as GadgetLog and restructuring the toolbox to support better tool registration and management.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Refactored agent tool and toolbox architecture
|
|
||||||
- Unified logging into @gadget/api as GadgetLog
|
|
||||||
- Improved tool registration and management
|
|
||||||
- Reduced log spam through pre-task cleanup
|
|
||||||
- Agent, tools, toolbox, tool loop, AI environment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-05 (Monday)
|
|
||||||
|
|
||||||
**Basic Chat System Implementation**
|
|
||||||
|
|
||||||
This week marked a major milestone with the implementation of a working basic chat system, including provider, model, and mode selection capabilities, along with enhanced AgentService.process method functionality.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented basic chat system
|
|
||||||
- Added provider, model, and mode selections
|
|
||||||
- Enhanced AgentService.process method
|
|
||||||
- Implemented chat turn processing
|
|
||||||
- Added agent progress tracking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-03 (Saturday)
|
|
||||||
|
|
||||||
**Status Message Display Fix**
|
|
||||||
|
|
||||||
This weekend's work focused on fixing the status message display in the agent interface and implementing workspace mode management improvements with drone status message socket events.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Fixed status message display in agent interface
|
|
||||||
- Implemented workspace mode management
|
|
||||||
- Added drone status message socket events
|
|
||||||
- Many fixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-02 (Friday)
|
|
||||||
|
|
||||||
**Workspace Mode Switching**
|
|
||||||
|
|
||||||
This week involved implementing workspace mode switching functionality with proper validation, ensuring that workspace mode changes are validated before being applied, preventing invalid state transitions.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented workspace mode switching with validation
|
|
||||||
- Added session lock and workspace processing refinements
|
|
||||||
- Improved workspace management system
|
|
||||||
- Workspace management progress
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-05-01 (Thursday)
|
|
||||||
|
|
||||||
**Welcome to The Grid**
|
|
||||||
|
|
||||||
This week marked the beginning of a new phase with the implementation of the "Welcome to The Grid" home view, including JWT processing fixes, session startup/shutdown/refactoring improvements, and the transition from Types.ObjectId to GadgetId.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented "Welcome to The Grid" home view
|
|
||||||
- Fixed JWT processing
|
|
||||||
- Refactored session startup, shutdown, and import
|
|
||||||
- Transitioned from Types.ObjectId to GadgetId
|
|
||||||
- Added return to Home on sign out
|
|
||||||
- More populate fixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-04-30 (Wednesday)
|
|
||||||
|
|
||||||
**Socket Protocol Implementation**
|
|
||||||
|
|
||||||
This week focused on implementing and refining the socket protocol for drone-to-IDE communication, including defining missing socket event types, enforcing typed events in the frontend build, and adding a setting for max Socket.IO HTTP buffer size.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented socket protocol completeness
|
|
||||||
- Defined missing socket event types
|
|
||||||
- Enforced typed events in frontend build
|
|
||||||
- Added max Socket.IO HTTP buffer size setting
|
|
||||||
- Created drone manager
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-04-29 (Tuesday)
|
|
||||||
|
|
||||||
**Workspace Persistence and Crash Recovery**
|
|
||||||
|
|
||||||
This week involved implementing workspace persistence and crash recovery capabilities, including adding workspace mode to the gadget-drone process, event emissions to the Agentic Workflow Loop, and drone-to-IDE event routing.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented workspace persistence and crash recovery
|
|
||||||
- Added workspace mode to gadget-drone process
|
|
||||||
- Added event emissions to Agentic Workflow Loop
|
|
||||||
- Implemented drone-to-IDE event routing
|
|
||||||
- Fixed type conflicts and implemented prompt submission
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-04-28 (Monday)
|
|
||||||
|
|
||||||
**Dark Industrial Theme and JWT Authentication**
|
|
||||||
|
|
||||||
This week marked a significant visual and architectural milestone with the implementation of the dark industrial theme, Project Manager, and JWT authentication, replacing the previous sign-up concept with a more streamlined sign-in experience.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Implemented dark industrial theme
|
|
||||||
- Added Project Manager
|
|
||||||
- Implemented JWT authentication
|
|
||||||
- Enforced theme correctly
|
|
||||||
- Added project service
|
|
||||||
- Updated DroneRegistration with workspaceDir
|
|
||||||
- Fix: Darken border colors to match dark industrial theme
|
|
||||||
- Theme enforced and correctly implemented; e2e tests added for theme
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-04-27 (Sunday)
|
|
||||||
|
|
||||||
**Initial Commit**
|
|
||||||
|
|
||||||
The project was initialized with the initial commit, establishing the foundation for all subsequent development.
|
|
||||||
|
|
||||||
### Changes:
|
|
||||||
- Initial project commit
|
|
||||||
- Established basic project structure
|
|
||||||
@ -22,12 +22,6 @@ or is aborted, the abort signal mechanism is cleaned up end-to-end.
|
|||||||
|
|
|
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
|
|
|
|
||||||
AiService.abort()
|
|
||||||
(calls AI provider's
|
|
||||||
abort() which for
|
|
||||||
Ollama terminates the
|
|
||||||
HTTP request immediately)
|
|
||||||
|
|
|
||||||
cb(true, "Abort signaled")
|
cb(true, "Abort signaled")
|
||||||
|
|
|
|
||||||
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
||||||
@ -80,10 +74,6 @@ 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 ...`
|
||||||
@ -91,9 +81,6 @@ in-progress HTTP requests.
|
|||||||
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`
|
||||||
@ -121,18 +108,7 @@ 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()` then `AiService.abort()`, returns `true` if
|
`this.abortController?.abort()`, returns `true` if there was a controller
|
||||||
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`**:
|
||||||
|
|
||||||
@ -217,11 +193,10 @@ 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`; `abort()` on `AiApi` |
|
| `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; `abort()` override |
|
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through |
|
||||||
| `packages/ai/src/openai.ts` | Same for `generate()`, `readStreamingChatCompletion()`, `readNonStreamingChatCompletion()` |
|
| `packages/ai/src/openai.ts` | Same for `generate()`, `readStreamingChatCompletion()`, `readNonStreamingChatCompletion()` |
|
||||||
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` calls `AiService.abort()` |
|
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` |
|
||||||
| `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` |
|
||||||
@ -239,16 +214,9 @@ 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 and
|
- The abort callback returns immediately after the controller is signaled. The
|
||||||
`AiService.abort()` is called. The AbortError propagates asynchronously
|
AbortError propagates asynchronously through the SDK stream and is caught
|
||||||
through the SDK stream and is caught separately in the agent loop.
|
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
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import type { Project } from '../lib/api';
|
|
||||||
|
|
||||||
interface ProjectPanelProps {
|
|
||||||
project: Project | null;
|
|
||||||
onNavigateToProject: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectPanel({ project, onNavigateToProject }: ProjectPanelProps) {
|
|
||||||
return (
|
|
||||||
<div className="border-b border-border-subtle shrink-0">
|
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
|
||||||
Project
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
title="Project Manager"
|
|
||||||
onClick={onNavigateToProject}
|
|
||||||
className="text-text-muted hover:text-text-primary transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-2">
|
|
||||||
{project ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Name</div>
|
|
||||||
<div className="text-text-primary">{project.name}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Slug</div>
|
|
||||||
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-text-muted text-sm">Loading...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,298 +0,0 @@
|
|||||||
import { ChatSessionMode, type AiProvider, type ChatSession, type AiModel } from '../lib/api';
|
|
||||||
import { WorkspaceMode } from '../lib/types';
|
|
||||||
import WorkspaceModeIndicator from './WorkspaceModeIndicator';
|
|
||||||
|
|
||||||
interface SessionPanelProps {
|
|
||||||
session: ChatSession | null;
|
|
||||||
workspaceMode: WorkspaceMode;
|
|
||||||
sessionLocked: boolean;
|
|
||||||
providers: AiProvider[];
|
|
||||||
selectedProviderId: string;
|
|
||||||
selectedModelId: string;
|
|
||||||
sessionReasoningEffort: string;
|
|
||||||
sessionNumCtx: number;
|
|
||||||
isEditingProvider: boolean;
|
|
||||||
editProviderId: string;
|
|
||||||
editModelId: string;
|
|
||||||
isUpdatingProvider: boolean;
|
|
||||||
isEditingName: boolean;
|
|
||||||
editName: string;
|
|
||||||
isUpdatingName: boolean;
|
|
||||||
isUpdatingMode: boolean;
|
|
||||||
isUpdatingReasoning: boolean;
|
|
||||||
isUpdatingNumCtx: boolean;
|
|
||||||
onWorkspaceModeChange: (mode: WorkspaceMode) => void;
|
|
||||||
onModeChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
||||||
onReasoningChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
||||||
onNumCtxChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
onNumCtxCommit: (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => void;
|
|
||||||
onStartEditProvider: () => void;
|
|
||||||
onSaveProvider: () => void;
|
|
||||||
onCancelEditProvider: () => void;
|
|
||||||
onEditProviderIdChange: (id: string) => void;
|
|
||||||
onEditModelIdChange: (id: string) => void;
|
|
||||||
onStartEditName: () => void;
|
|
||||||
onSaveName: () => void;
|
|
||||||
onCancelEditName: () => void;
|
|
||||||
onEditNameChange: (name: string) => void;
|
|
||||||
getProviderName: (id: string) => string;
|
|
||||||
getModelName: (providerId: string, modelId: string) => string;
|
|
||||||
getSortedModels: (providerId: string) => AiProvider['models'];
|
|
||||||
truncateModelName: (name: string, maxLength?: number) => string;
|
|
||||||
getSelectedModelCapabilities: () => { hasThinking: boolean } | null;
|
|
||||||
getSelectedModel: () => AiModel | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SessionPanel({
|
|
||||||
session,
|
|
||||||
workspaceMode,
|
|
||||||
sessionLocked,
|
|
||||||
providers,
|
|
||||||
selectedProviderId,
|
|
||||||
selectedModelId,
|
|
||||||
sessionReasoningEffort,
|
|
||||||
sessionNumCtx,
|
|
||||||
isEditingProvider,
|
|
||||||
editProviderId,
|
|
||||||
editModelId,
|
|
||||||
isUpdatingProvider,
|
|
||||||
isEditingName,
|
|
||||||
editName,
|
|
||||||
isUpdatingName,
|
|
||||||
isUpdatingMode,
|
|
||||||
isUpdatingReasoning,
|
|
||||||
isUpdatingNumCtx,
|
|
||||||
onWorkspaceModeChange,
|
|
||||||
onModeChange,
|
|
||||||
onReasoningChange,
|
|
||||||
onNumCtxChange,
|
|
||||||
onNumCtxCommit,
|
|
||||||
onStartEditProvider,
|
|
||||||
onSaveProvider,
|
|
||||||
onCancelEditProvider,
|
|
||||||
onEditProviderIdChange,
|
|
||||||
onEditModelIdChange,
|
|
||||||
onStartEditName,
|
|
||||||
onSaveName,
|
|
||||||
onCancelEditName,
|
|
||||||
onEditNameChange,
|
|
||||||
getProviderName,
|
|
||||||
getModelName,
|
|
||||||
getSortedModels,
|
|
||||||
truncateModelName,
|
|
||||||
getSelectedModelCapabilities,
|
|
||||||
getSelectedModel,
|
|
||||||
}: SessionPanelProps) {
|
|
||||||
return (
|
|
||||||
<div className="border-b border-border-subtle shrink-0">
|
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
|
||||||
Session
|
|
||||||
</h3>
|
|
||||||
<WorkspaceModeIndicator
|
|
||||||
mode={workspaceMode}
|
|
||||||
onChange={onWorkspaceModeChange}
|
|
||||||
disabled={!sessionLocked || isEditingProvider}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Name</div>
|
|
||||||
{isEditingName ? (
|
|
||||||
<div className="flex gap-2 mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editName}
|
|
||||||
onChange={(e) => onEditNameChange(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') { e.preventDefault(); onSaveName(); }
|
|
||||||
if (e.key === 'Escape') { e.preventDefault(); onCancelEditName(); }
|
|
||||||
}}
|
|
||||||
className="flex-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
title="Save"
|
|
||||||
onClick={onSaveName}
|
|
||||||
disabled={!editName.trim() || isUpdatingName}
|
|
||||||
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors self-center"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Cancel"
|
|
||||||
onClick={onCancelEditName}
|
|
||||||
disabled={isUpdatingName}
|
|
||||||
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors self-center"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2 items-center mt-1">
|
|
||||||
<div className="text-text-primary flex-1">{session?.name}</div>
|
|
||||||
<button
|
|
||||||
title="Edit name"
|
|
||||||
onClick={onStartEditName}
|
|
||||||
className="text-text-muted hover:text-text-primary transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isEditingProvider ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-xs text-text-muted">Provider</div>
|
|
||||||
<select
|
|
||||||
value={editProviderId || ''}
|
|
||||||
onChange={(e) => { onEditProviderIdChange(e.target.value); onEditModelIdChange(''); }}
|
|
||||||
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none"
|
|
||||||
>
|
|
||||||
<option value="">-- Select Provider --</option>
|
|
||||||
{providers.map((provider) => (
|
|
||||||
<option key={provider._id} value={provider._id}>
|
|
||||||
{provider.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-xs text-text-muted">Model</div>
|
|
||||||
<select
|
|
||||||
value={editModelId || ''}
|
|
||||||
onChange={(e) => onEditModelIdChange(e.target.value)}
|
|
||||||
disabled={!editProviderId}
|
|
||||||
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed truncate"
|
|
||||||
>
|
|
||||||
{!editProviderId ? (
|
|
||||||
<option value="">Select provider first</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">-- Select Model --</option>
|
|
||||||
{getSortedModels(editProviderId).map((model) => (
|
|
||||||
<option key={model.id} value={model.id}>
|
|
||||||
{truncateModelName(model.name)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end gap-1 pb-1">
|
|
||||||
<button
|
|
||||||
title="Save"
|
|
||||||
onClick={onSaveProvider}
|
|
||||||
disabled={!editProviderId || !editModelId || isUpdatingProvider}
|
|
||||||
className="text-green-500 hover:text-green-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Cancel"
|
|
||||||
onClick={onCancelEditProvider}
|
|
||||||
disabled={isUpdatingProvider}
|
|
||||||
className="text-red-500 hover:text-red-400 disabled:opacity-30 transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-xs text-text-muted">Provider</div>
|
|
||||||
<div className="text-text-primary text-sm mt-1">{getProviderName(selectedProviderId)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-xs text-text-muted">Model</div>
|
|
||||||
<div
|
|
||||||
className="text-text-primary text-sm mt-1 truncate"
|
|
||||||
title={getModelName(selectedProviderId, selectedModelId)}
|
|
||||||
>
|
|
||||||
{getModelName(selectedProviderId, selectedModelId)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
title="Change provider/model..."
|
|
||||||
onClick={onStartEditProvider}
|
|
||||||
className="self-end pb-1 text-text-muted hover:text-text-primary transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Mode</div>
|
|
||||||
<select
|
|
||||||
value={session?.mode || ChatSessionMode.Build}
|
|
||||||
onChange={onModeChange}
|
|
||||||
disabled={isUpdatingMode}
|
|
||||||
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{Object.values(ChatSessionMode).map((mode) => (
|
|
||||||
<option key={mode} value={mode}>
|
|
||||||
{mode.charAt(0).toUpperCase() + mode.slice(1)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Reasoning</div>
|
|
||||||
<select
|
|
||||||
value={sessionReasoningEffort}
|
|
||||||
onChange={onReasoningChange}
|
|
||||||
disabled={isUpdatingReasoning || !getSelectedModelCapabilities()?.hasThinking}
|
|
||||||
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
title={
|
|
||||||
!getSelectedModelCapabilities()?.hasThinking
|
|
||||||
? "Selected model does not support reasoning"
|
|
||||||
: "Controls how much the model thinks before responding"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="off">Off</option>
|
|
||||||
<option value="low">Low</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="high">High</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{providers.find(p => p._id === selectedProviderId)?.apiType !== 'openai' && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Context Window</div>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={16384}
|
|
||||||
max={getSelectedModel()?.contextWindow || 131072}
|
|
||||||
step={8192}
|
|
||||||
value={sessionNumCtx}
|
|
||||||
onChange={onNumCtxChange}
|
|
||||||
onMouseUp={onNumCtxCommit}
|
|
||||||
onTouchEnd={onNumCtxCommit}
|
|
||||||
className="flex-1 h-2 bg-bg-tertiary rounded appearance-none cursor-pointer accent-brand"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-text-secondary w-16 text-right tabular-nums">
|
|
||||||
{sessionNumCtx.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -358,7 +358,6 @@ 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 }>;
|
||||||
}
|
}
|
||||||
@ -422,7 +421,6 @@ 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,8 +3,7 @@ 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 SessionPanel from '../components/SessionPanel';
|
import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
|
||||||
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';
|
||||||
@ -68,8 +67,6 @@ 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('');
|
||||||
@ -217,15 +214,6 @@ export default function ChatSessionView() {
|
|||||||
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');
|
||||||
appContext?.setStatusMessage('Connecting...');
|
appContext?.setStatusMessage('Connecting...');
|
||||||
@ -804,16 +792,13 @@ export default function ChatSessionView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedModel = useCallback(() => {
|
const getSelectedModelCapabilities = 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;
|
||||||
return provider.models.find(m => m.id === selectedModelId) || null;
|
const model = provider.models.find(m => m.id === selectedModelId);
|
||||||
|
return model?.capabilities || null;
|
||||||
}, [providers, selectedProviderId, selectedModelId]);
|
}, [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;
|
||||||
|
|
||||||
@ -835,28 +820,6 @@ export default function ChatSessionView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNumCtxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSessionNumCtx(parseInt(e.target.value, 10));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNumCtxCommit = async (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => {
|
|
||||||
if (!session || isUpdatingNumCtx) return;
|
|
||||||
|
|
||||||
const newValue = parseInt(e.currentTarget.value, 10);
|
|
||||||
setIsUpdatingNumCtx(true);
|
|
||||||
try {
|
|
||||||
const updatedSession = await chatSessionApi.update(session._id, {
|
|
||||||
numCtx: newValue
|
|
||||||
});
|
|
||||||
setSession(updatedSession);
|
|
||||||
showToast(`Context window set to ${newValue.toLocaleString()}`);
|
|
||||||
} catch (err) {
|
|
||||||
showToast(`Failed to change context window: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
||||||
} finally {
|
|
||||||
setIsUpdatingNumCtx(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProviderName = (id: string) => {
|
const getProviderName = (id: string) => {
|
||||||
return providers.find(p => p._id === id)?.name ?? 'None';
|
return providers.find(p => p._id === id)?.name ?? 'None';
|
||||||
};
|
};
|
||||||
@ -895,13 +858,6 @@ 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) {
|
||||||
@ -1208,52 +1164,232 @@ 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">
|
||||||
<SessionPanel
|
{/* SESSION Panel */}
|
||||||
session={session}
|
<div className="border-b border-border-subtle shrink-0">
|
||||||
workspaceMode={workspaceMode}
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
sessionLocked={sessionLocked}
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
providers={providers}
|
Session
|
||||||
selectedProviderId={selectedProviderId}
|
</h3>
|
||||||
selectedModelId={selectedModelId}
|
<WorkspaceModeIndicator
|
||||||
sessionReasoningEffort={sessionReasoningEffort}
|
mode={workspaceMode}
|
||||||
sessionNumCtx={sessionNumCtx}
|
onChange={handleWorkspaceModeChange}
|
||||||
isEditingProvider={isEditingProvider}
|
disabled={!sessionLocked || 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}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<ProjectPanel
|
<div className="p-4 space-y-3">
|
||||||
project={project}
|
<div>
|
||||||
onNavigateToProject={() => navigate(`/projects/${project?.slug}`)}
|
<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>
|
||||||
|
|
||||||
|
{/* PROJECT Panel */}
|
||||||
|
<div className="border-b border-border-subtle shrink-0">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Project
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
title="Project Manager"
|
||||||
|
onClick={() => navigate(`/projects/${project?.slug}`)}
|
||||||
|
className="text-text-muted hover:text-text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
{project ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Name</div>
|
||||||
|
<div className="text-text-primary">{project.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Slug</div>
|
||||||
|
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-text-muted text-sm">Loading...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FILES Panel */}
|
||||||
<FilesPanel
|
<FilesPanel
|
||||||
workspaceMode={workspaceMode}
|
workspaceMode={workspaceMode}
|
||||||
onFileSelect={(path) => {
|
onFileSelect={(path) => {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ 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;
|
||||||
@ -232,11 +231,7 @@ interface ProjectInspectorProps {
|
|||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectInspector({
|
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
|
||||||
project,
|
|
||||||
onDelete,
|
|
||||||
onUpdate,
|
|
||||||
}: ProjectInspectorProps) {
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
@ -284,9 +279,7 @@ function ProjectInspector({
|
|||||||
</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">
|
<div className="font-mono text-text-primary">{project.slug}</div>
|
||||||
{project.slug}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -659,38 +652,9 @@ 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-2xl flex gap-6">
|
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-md">
|
||||||
<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>
|
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
|
||||||
{selectedDrone && (
|
{selectedDrone && (
|
||||||
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
|
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
|
||||||
@ -750,14 +714,10 @@ function NewChatSessionModal({
|
|||||||
disabled={selectedProvider.models.length === 0}
|
disabled={selectedProvider.models.length === 0}
|
||||||
>
|
>
|
||||||
<option value="">Select a model</option>
|
<option value="">Select a model</option>
|
||||||
{[...selectedProvider.models]
|
{[...selectedProvider.models].sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.map((model) => (
|
|
||||||
<option key={model.id} value={model.id}>
|
<option key={model.id} value={model.id}>
|
||||||
{model.name}{" "}
|
{model.name}{" "}
|
||||||
{model.parameterLabel
|
{model.parameterLabel ? `(${model.parameterLabel})` : ""}
|
||||||
? `(${model.parameterLabel})`
|
|
||||||
: ""}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@ -806,7 +766,6 @@ function NewChatSessionModal({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,9 +774,7 @@ 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>(
|
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showNewForm, setShowNewForm] = useState(false);
|
const [showNewForm, setShowNewForm] = useState(false);
|
||||||
|
|
||||||
@ -882,10 +839,7 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
|||||||
console.error("Failed to lock drone session");
|
console.error("Failed to lock drone session");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem('dtp_drone_registration', JSON.stringify(selectedDrone));
|
||||||
"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);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 210 KiB |
29
gadget-code/frontend/src/types/assets.d.ts
vendored
29
gadget-code/frontend/src/types/assets.d.ts
vendored
@ -1,29 +0,0 @@
|
|||||||
declare module "*.jpeg" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.jpg" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.png" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.gif" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.svg" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.webp" {
|
|
||||||
const src: string;
|
|
||||||
export default src;
|
|
||||||
}
|
|
||||||
@ -179,7 +179,6 @@ 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) {
|
||||||
@ -205,12 +204,6 @@ 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,7 +30,6 @@ 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,7 +69,6 @@ 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,7 +202,6 @@ 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);
|
||||||
@ -231,9 +230,6 @@ 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();
|
||||||
|
|
||||||
@ -280,7 +276,6 @@ 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, turn.numCtx),
|
this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning),
|
||||||
chatOptions,
|
chatOptions,
|
||||||
this.makeStreamHandler(socket),
|
this.makeStreamHandler(socket),
|
||||||
);
|
);
|
||||||
@ -337,7 +337,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,7 +473,6 @@ 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 {
|
||||||
@ -485,7 +483,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: numCtxOverride ?? settings?.numCtx ?? 131072, // session override > model settings > 128k fallback
|
numCtx: settings?.numCtx ?? 131072, // 128k fallback
|
||||||
maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback
|
maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -627,7 +625,7 @@ class AgentService extends GadgetService {
|
|||||||
|
|
||||||
const response = await AiService.chat(
|
const response = await AiService.chat(
|
||||||
provider,
|
provider,
|
||||||
this.buildDroneModelConfig(provider, turn.llm, false, turn.numCtx),
|
this.buildDroneModelConfig(provider, turn.llm, false),
|
||||||
chatOptions,
|
chatOptions,
|
||||||
streamHandler,
|
streamHandler,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,7 +18,6 @@ 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,
|
||||||
@ -48,8 +47,6 @@ 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");
|
||||||
}
|
}
|
||||||
@ -106,11 +103,7 @@ 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 };
|
||||||
try {
|
return api.generate(modelConfig, options, streamCallback);
|
||||||
return await api.generate(modelConfig, options, streamCallback);
|
|
||||||
} finally {
|
|
||||||
this.activeApi = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async chat(
|
async chat(
|
||||||
@ -128,23 +121,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 };
|
||||||
try {
|
|
||||||
return await api.chat(modelConfig, options, streamCallback);
|
return await api.chat(modelConfig, options, streamCallback);
|
||||||
} finally {
|
|
||||||
this.activeApi = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getApi(provider: AiProviderConfig) {
|
getApi(provider: AiProviderConfig) {
|
||||||
const api = createAiApi(aiEnv, provider, this.log);
|
return 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,15 +196,6 @@ 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,11 +34,6 @@ export class OllamaAiApi extends AiApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Forcefully abort any in-progress Ollama API request. */
|
|
||||||
override abort(): void {
|
|
||||||
this.client.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
async listModels(): Promise<IAiModelListResult> {
|
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,7 +35,6 @@ 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,7 +86,6 @@ 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;
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* forever-sleep.js
|
|
||||||
* A script that sleeps, wakes up every 5 seconds, logs to stdout, then goes back to sleep.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('forever-sleep: Starting...');
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`forever-sleep: ${timestamp} - Waking up (sleeping for 5 seconds)...`);
|
|
||||||
|
|
||||||
await sleep(5000);
|
|
||||||
|
|
||||||
const timestampAfter = new Date().toISOString();
|
|
||||||
console.log(`forever-sleep: ${timestampAfter} - Back to sleep...`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error('forever-sleep: Error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user