mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
chore: commit remaining workspace updates
This commit is contained in:
parent
98964f80ff
commit
3ba4c54c0c
4 changed files with 99 additions and 16 deletions
|
|
@ -70,7 +70,7 @@ Use `offset` to track the last seen `sequence` number and resume from where you
|
||||||
|
|
||||||
### Bare minimum
|
### Bare minimum
|
||||||
|
|
||||||
Handle these three events to render a basic chat:
|
Handle item lifecycle plus turn lifecycle to render a basic chat:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type ItemState = {
|
type ItemState = {
|
||||||
|
|
@ -79,9 +79,20 @@ type ItemState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = new Map<string, ItemState>();
|
const items = new Map<string, ItemState>();
|
||||||
|
let turnInProgress = false;
|
||||||
|
|
||||||
function handleEvent(event: UniversalEvent) {
|
function handleEvent(event: UniversalEvent) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case "turn.started": {
|
||||||
|
turnInProgress = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "turn.ended": {
|
||||||
|
turnInProgress = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "item.started": {
|
case "item.started": {
|
||||||
const { item } = event.data as ItemEventData;
|
const { item } = event.data as ItemEventData;
|
||||||
items.set(item.item_id, { item, deltas: [] });
|
items.set(item.item_id, { item, deltas: [] });
|
||||||
|
|
@ -110,12 +121,14 @@ function handleEvent(event: UniversalEvent) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When rendering, show a loading indicator while `item.status === "in_progress"`:
|
When rendering:
|
||||||
|
- Use `turnInProgress` for turn-level UI state (disable send button, show global "Agent is responding", etc.).
|
||||||
|
- Use `item.status === "in_progress"` for per-item streaming state.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function renderItem(state: ItemState) {
|
function renderItem(state: ItemState) {
|
||||||
const { item, deltas } = state;
|
const { item, deltas } = state;
|
||||||
const isLoading = item.status === "in_progress";
|
const isItemLoading = item.status === "in_progress";
|
||||||
|
|
||||||
// For streaming text, combine item content with accumulated deltas
|
// For streaming text, combine item content with accumulated deltas
|
||||||
const text = item.content
|
const text = item.content
|
||||||
|
|
@ -126,7 +139,8 @@ function renderItem(state: ItemState) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: streamedText,
|
content: streamedText,
|
||||||
isLoading,
|
isItemLoading,
|
||||||
|
isTurnLoading: turnInProgress,
|
||||||
role: item.role,
|
role: item.role,
|
||||||
kind: item.kind,
|
kind: item.kind,
|
||||||
};
|
};
|
||||||
|
|
@ -155,16 +169,6 @@ function handleEvent(event: UniversalEvent) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "turn.started": {
|
|
||||||
// Turn began (useful for showing per-turn loading state)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "turn.ended": {
|
|
||||||
// Turn completed (useful for ending per-turn loading state)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "error": {
|
case "error": {
|
||||||
const { message, code } = event.data as ErrorData;
|
const { message, code } = event.data as ErrorData;
|
||||||
// Display error to user
|
// Display error to user
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const OPENCODE_EVENT_LOG_SIZE: usize = 4096;
|
||||||
const OPENCODE_DEFAULT_MODEL_ID: &str = "mock";
|
const OPENCODE_DEFAULT_MODEL_ID: &str = "mock";
|
||||||
const OPENCODE_DEFAULT_PROVIDER_ID: &str = "mock";
|
const OPENCODE_DEFAULT_PROVIDER_ID: &str = "mock";
|
||||||
const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
|
const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
|
||||||
|
const OPENCODE_MODEL_CHANGE_AFTER_SESSION_CREATE_ERROR: &str = "OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session.";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct OpenCodeStreamEvent {
|
struct OpenCodeStreamEvent {
|
||||||
|
|
@ -668,6 +669,12 @@ struct OpenCodeCreateSessionRequest {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct OpenCodeUpdateSessionRequest {
|
struct OpenCodeUpdateSessionRequest {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
#[schema(value_type = String)]
|
||||||
|
model: Option<Value>,
|
||||||
|
#[serde(rename = "providerID", alias = "provider_id")]
|
||||||
|
provider_id: Option<String>,
|
||||||
|
#[serde(rename = "modelID", alias = "model_id")]
|
||||||
|
model_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, IntoParams)]
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
|
@ -3850,11 +3857,30 @@ async fn oc_session_get(
|
||||||
async fn oc_session_update(
|
async fn oc_session_update(
|
||||||
State(state): State<Arc<OpenCodeAppState>>,
|
State(state): State<Arc<OpenCodeAppState>>,
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
Json(body): Json<OpenCodeUpdateSessionRequest>,
|
Json(body): Json<Value>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut sessions = state.opencode.sessions.lock().await;
|
let mut sessions = state.opencode.sessions.lock().await;
|
||||||
if let Some(session) = sessions.get_mut(&session_id) {
|
if let Some(session) = sessions.get_mut(&session_id) {
|
||||||
if let Some(title) = body.title {
|
let requests_model_change = body
|
||||||
|
.as_object()
|
||||||
|
.map(|obj| {
|
||||||
|
obj.contains_key("model")
|
||||||
|
|| obj.contains_key("providerID")
|
||||||
|
|| obj.contains_key("modelID")
|
||||||
|
|| obj.contains_key("provider_id")
|
||||||
|
|| obj.contains_key("model_id")
|
||||||
|
|| obj.contains_key("providerId")
|
||||||
|
|| obj.contains_key("modelId")
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
if requests_model_change {
|
||||||
|
return bad_request(OPENCODE_MODEL_CHANGE_AFTER_SESSION_CREATE_ERROR).into_response();
|
||||||
|
}
|
||||||
|
if let Some(title) = body
|
||||||
|
.get("title")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
{
|
||||||
if let Err(err) = state
|
if let Err(err) = state
|
||||||
.inner
|
.inner
|
||||||
.session_manager()
|
.session_manager()
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,29 @@ describe("OpenCode-compatible Session API", () => {
|
||||||
const response = await client.session.get({ path: { id: sessionId } });
|
const response = await client.session.get({ path: { id: sessionId } });
|
||||||
expect(response.data?.title).toBe("Updated");
|
expect(response.data?.title).toBe("Updated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should reject model changes after session creation", async () => {
|
||||||
|
const created = await client.session.create({ body: { title: "Original" } });
|
||||||
|
const sessionId = created.data?.id!;
|
||||||
|
|
||||||
|
const response = await fetch(`${handle.baseUrl}/opencode/session/${sessionId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${handle.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
providerID: "codex",
|
||||||
|
modelID: "gpt-5",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(data?.errors?.[0]?.message).toBe(
|
||||||
|
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session."
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("session.delete", () => {
|
describe("session.delete", () => {
|
||||||
|
|
|
||||||
|
|
@ -519,8 +519,36 @@
|
||||||
"daemon"
|
"daemon"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"TurnEventData": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"phase"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"metadata": true,
|
||||||
|
"phase": {
|
||||||
|
"$ref": "#/definitions/TurnPhase"
|
||||||
|
},
|
||||||
|
"turn_id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TurnPhase": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"started",
|
||||||
|
"ended"
|
||||||
|
]
|
||||||
|
},
|
||||||
"UniversalEventData": {
|
"UniversalEventData": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/TurnEventData"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/SessionStartedData"
|
"$ref": "#/definitions/SessionStartedData"
|
||||||
},
|
},
|
||||||
|
|
@ -552,6 +580,8 @@
|
||||||
"enum": [
|
"enum": [
|
||||||
"session.started",
|
"session.started",
|
||||||
"session.ended",
|
"session.ended",
|
||||||
|
"turn.started",
|
||||||
|
"turn.ended",
|
||||||
"item.started",
|
"item.started",
|
||||||
"item.delta",
|
"item.delta",
|
||||||
"item.completed",
|
"item.completed",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue