added Gab AI affiliate link; changed Ollama abort procedure
This commit is contained in:
parent
96663f7c88
commit
d990d0dd71
@ -22,6 +22,12 @@ 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..." -----------------------+
|
||||||
@ -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
|
||||||
|
|||||||
@ -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,9 +659,38 @@ 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">
|
||||||
|
<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">
|
||||||
@ -714,10 +750,14 @@ 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].sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
|
{[...selectedProvider.models]
|
||||||
|
.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>
|
||||||
@ -766,6 +806,7 @@ function NewChatSessionModal({
|
|||||||
</form>
|
</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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 };
|
||||||
|
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) {
|
||||||
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) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user