Add max iterations

This commit is contained in:
dwrz
2026-07-05 17:55:07 +00:00
parent c4730eb8ce
commit 1e4c7f0334
6 changed files with 95 additions and 2 deletions

View File

@@ -446,6 +446,8 @@ type Request struct {
Model string `json:"model,omitempty"` Model string `json:"model,omitempty"`
// SystemMessage overrides the configured system message for this request. // SystemMessage overrides the configured system message for this request.
SystemMessage string `json:"system_message,omitempty"` SystemMessage string `json:"system_message,omitempty"`
// MaxIterations caps the number of agent loop turns (0 = unlimited).
MaxIterations int `json:"max_iterations,omitempty"`
} }
// Response is the response format for chat and voice endpoints. // Response is the response format for chat and voice endpoints.
@@ -541,7 +543,7 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
} }
res, err := llmc.Completions( res, err := llmc.Completions(
ctx, model, req.SystemMessage, 0, req.Messages, ctx, model, req.SystemMessage, req.MaxIterations, req.Messages,
) )
if err != nil { if err != nil {
log.ErrorContext( log.ErrorContext(
@@ -713,7 +715,7 @@ func (svc *Service) chatStream(w http.ResponseWriter, r *http.Request) {
) )
go func() { go func() {
errs <- llmc.CompletionsStream( errs <- llmc.CompletionsStream(
ctx, model, req.SystemMessage, 0, req.Messages, events, ctx, model, req.SystemMessage, req.MaxIterations, req.Messages, events,
) )
close(errs) close(errs)
}() }()

View File

@@ -134,4 +134,10 @@
<path d="M7 15h6"/> <path d="M7 15h6"/>
<path d="M7 7h8"/> <path d="M7 7h8"/>
</symbol> </symbol>
<symbol id="settings-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 17H5" />
<path d="M19 7h-9" />
<circle cx="17" cy="17" r="3" />
<circle cx="7" cy="7" r="3" />
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -661,6 +661,18 @@ body {
} }
} }
#compose-settings-close {
border: none;
color: var(--color-text);
}
@media (hover: hover) {
#compose-settings-close:hover {
border-color: transparent;
color: var(--color-text);
}
}
/* Tabs */ /* Tabs */
.settings-tabs { .settings-tabs {
display: flex; display: flex;
@@ -715,6 +727,14 @@ body {
margin-bottom: var(--s-2); margin-bottom: var(--s-2);
} }
.settings-label__sub {
display: block;
font-size: var(--s-2);
font-weight: 400;
color: var(--color-text-muted);
margin-top: 0.125rem;
}
.settings-select { .settings-select {
width: 100%; width: 100%;
height: var(--s2); height: var(--s2);
@@ -735,6 +755,10 @@ body {
outline-offset: 2px; outline-offset: 2px;
} }
.settings-field {
margin-bottom: var(--s1);
}
.settings-textarea { .settings-textarea {
display: block; display: block;
width: 100%; width: 100%;

View File

@@ -14,6 +14,7 @@ import { WakeLock } from './wakelock.js';
const STREAM_ENDPOINT = '/v1/chat/voice/stream'; const STREAM_ENDPOINT = '/v1/chat/voice/stream';
const SYSTEM_MESSAGE_KEY = 'odidere_system_message'; const SYSTEM_MESSAGE_KEY = 'odidere_system_message';
const MAX_TURNS_KEY = 'odidere_max_turns';
const ICONS_URL = '/static/icons.svg'; const ICONS_URL = '/static/icons.svg';
const StreamEventDelta = 'delta'; const StreamEventDelta = 'delta';
@@ -102,6 +103,13 @@ class Odidere {
this.$settingsClose = document.getElementById('settings-close'); this.$settingsClose = document.getElementById('settings-close');
this.$settingsTabs = document.querySelectorAll('.settings-tab'); this.$settingsTabs = document.querySelectorAll('.settings-tab');
this.$saveSystemMessage = document.getElementById('save-system-message'); this.$saveSystemMessage = document.getElementById('save-system-message');
this.$composeSettingsOverlay = document.getElementById(
'compose-settings-overlay',
);
this.$composeSettingsClose = document.getElementById(
'compose-settings-close',
);
this.$maxTurnsInput = document.getElementById('max-turns-input');
this.$compose = document.querySelector('.compose'); this.$compose = document.querySelector('.compose');
this.init(); this.init();
@@ -241,6 +249,18 @@ class Odidere {
this.settings.saveSystemMessage(), this.settings.saveSystemMessage(),
); );
// --- Compose settings ---
this.$composeSettingsClose.addEventListener('click', () =>
this._closeComposeSettings(),
);
this.$composeSettingsOverlay.addEventListener('click', (e) => {
if (e.target === this.$composeSettingsOverlay)
this._closeComposeSettings();
});
this.$maxTurnsInput.addEventListener('input', () => {
this._setMaxTurns(this.$maxTurnsInput.value);
});
// --- Model/voice change persistence --- // --- Model/voice change persistence ---
this.settings.$model.addEventListener('change', () => this.settings.$model.addEventListener('change', () =>
this.settings.saveModelSelection(), this.settings.saveModelSelection(),
@@ -297,6 +317,9 @@ class Odidere {
case 'settings': case 'settings':
this.settings.open(); this.settings.open();
break; break;
case 'compose-settings':
this._openComposeSettings();
break;
case 'attach': case 'attach':
// Handled by direct listener on $attach. // Handled by direct listener on $attach.
break; break;
@@ -599,6 +622,10 @@ class Odidere {
if (systemMessage) { if (systemMessage) {
payload.system_message = systemMessage; payload.system_message = systemMessage;
} }
const maxTurns = this._getMaxTurns();
if (maxTurns > 0) {
payload.max_iterations = maxTurns;
}
// Clear attachments and location after building payload. // Clear attachments and location after building payload.
this.clearAttachments(); this.clearAttachments();
@@ -1100,6 +1127,30 @@ class Odidere {
this.$location.classList.remove('compose__action-btn--location', 'active'); this.$location.classList.remove('compose__action-btn--location', 'active');
} }
// ====================
// COMPOSE SETTINGS
// ====================
_openComposeSettings() {
this.$maxTurnsInput.value = this._getMaxTurns();
this.$composeSettingsOverlay.classList.add('open');
this.$composeSettingsClose.focus();
}
_closeComposeSettings() {
this.$composeSettingsOverlay.classList.remove('open');
this.$composeSettings.focus();
}
_getMaxTurns() {
const stored = localStorage.getItem(MAX_TURNS_KEY);
return stored ? Number(stored) : 0;
}
_setMaxTurns(value) {
localStorage.setItem(MAX_TURNS_KEY, String(value));
}
// ==================== // ====================
// TWO-PRESS SAFETY PATTERNS // TWO-PRESS SAFETY PATTERNS
// ==================== // ====================

View File

@@ -4,5 +4,6 @@
{{ template "templates" . }} {{ template "templates" . }}
{{ template "modal/settings" . }} {{ template "modal/settings" . }}
{{ template "modal/edit" . }} {{ template "modal/edit" . }}
{{ template "modal/compose-settings" . }}
</body> </body>
{{ end }} {{ end }}

View File

@@ -10,6 +10,15 @@
<div class="compose__attachments" id="attachments"></div> <div class="compose__attachments" id="attachments"></div>
<div class="compose__actions"> <div class="compose__actions">
<div class="compose__actions-left"> <div class="compose__actions-left">
<button
type="button"
class="compose__action-btn"
id="compose-settings"
data-action="compose-settings"
aria-label="Compose settings"
>
<svg class="icon"><use href="/static/icons.svg#settings-2"></use></svg>
</button>
<button <button
type="button" type="button"
class="compose__action-btn" class="compose__action-btn"