mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step.
261 lines
7.5 KiB
TypeScript
261 lines
7.5 KiB
TypeScript
import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit'
|
|
import { parseClaudeModelList } from './claude-model-list-probe'
|
|
import { labelFromModelId } from './model-id-label'
|
|
import type { CommitMessageModel, ThinkingLevel } from './commit-message-agent-spec'
|
|
|
|
export const COMMIT_MESSAGE_MODEL_JSON_STRUCTURE_LIMITS = {
|
|
structuralTokens: 64 * 1024,
|
|
nestingDepth: 16
|
|
} as const
|
|
|
|
export const BASIC_THINKING_LEVELS: ThinkingLevel[] = [
|
|
{ id: 'low', label: 'Low' },
|
|
{ id: 'medium', label: 'Medium' },
|
|
{ id: 'high', label: 'High' }
|
|
]
|
|
|
|
export const OPENAI_THINKING_LEVELS: ThinkingLevel[] = [
|
|
{ id: 'low', label: 'Low' },
|
|
{ id: 'medium', label: 'Medium' },
|
|
{ id: 'high', label: 'High' },
|
|
{ id: 'xhigh', label: 'Extra High' }
|
|
]
|
|
|
|
export const CLAUDE_THINKING_LEVELS: ThinkingLevel[] = [
|
|
{ id: 'low', label: 'Low' },
|
|
{ id: 'medium', label: 'Medium' },
|
|
{ id: 'high', label: 'High' },
|
|
{ id: 'xhigh', label: 'Extra High' },
|
|
{ id: 'max', label: 'Max' }
|
|
]
|
|
|
|
function uniqueModels(models: CommitMessageModel[]): CommitMessageModel[] {
|
|
const seen = new Set<string>()
|
|
return models.filter((model) => {
|
|
if (!model.id || seen.has(model.id)) {
|
|
return false
|
|
}
|
|
seen.add(model.id)
|
|
return true
|
|
})
|
|
}
|
|
|
|
function* iterateModelOutputLines(output: string): Generator<string> {
|
|
let lineStart = 0
|
|
|
|
for (let index = 0; index < output.length; index++) {
|
|
const code = output.charCodeAt(index)
|
|
if (code !== 10 && code !== 13) {
|
|
continue
|
|
}
|
|
|
|
yield output.slice(lineStart, index)
|
|
if (code === 13 && output.charCodeAt(index + 1) === 10) {
|
|
index++
|
|
}
|
|
lineStart = index + 1
|
|
}
|
|
|
|
if (lineStart <= output.length) {
|
|
yield output.slice(lineStart)
|
|
}
|
|
}
|
|
|
|
export function withOpenAiThinking(
|
|
id: string
|
|
): Pick<CommitMessageModel, 'thinkingLevels' | 'defaultThinkingLevel'> {
|
|
return /(?:gpt-5|codex)/i.test(id)
|
|
? { thinkingLevels: OPENAI_THINKING_LEVELS, defaultThinkingLevel: 'low' }
|
|
: {}
|
|
}
|
|
|
|
export function parseClaudeModels(stdout: string): CommitMessageModel[] {
|
|
return uniqueModels(
|
|
parseClaudeModelList(stdout).map((model) => {
|
|
const thinkingLevels = CLAUDE_THINKING_LEVELS.filter((level) =>
|
|
model.effortLevels.includes(level.id)
|
|
)
|
|
return {
|
|
id: model.id,
|
|
label: model.label,
|
|
...(model.description ? { description: model.description } : {}),
|
|
...(thinkingLevels.length > 0
|
|
? {
|
|
thinkingLevels,
|
|
defaultThinkingLevel: thinkingLevels.some((level) => level.id === 'low')
|
|
? 'low'
|
|
: thinkingLevels[0].id
|
|
}
|
|
: {}),
|
|
...(model.supportsFastMode ? { supportsFastMode: true } : {}),
|
|
...(model.supportsAutoMode !== undefined
|
|
? { supportsAutoMode: model.supportsAutoMode }
|
|
: {})
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
export function parseCodexModels(stdout: string): CommitMessageModel[] {
|
|
try {
|
|
assertJsonTextStructureWithinLimits(stdout, COMMIT_MESSAGE_MODEL_JSON_STRUCTURE_LIMITS)
|
|
const parsed = JSON.parse(stdout) as {
|
|
models?: {
|
|
slug?: string
|
|
display_name?: string
|
|
supported_reasoning_levels?: { effort?: string }[]
|
|
default_reasoning_level?: string
|
|
}[]
|
|
}
|
|
return uniqueModels(
|
|
(parsed.models ?? [])
|
|
.filter((model) => model.slug && model.display_name)
|
|
.map((model) => ({
|
|
id: model.slug!,
|
|
label: model.display_name!,
|
|
...(model.supported_reasoning_levels?.length
|
|
? {
|
|
thinkingLevels: model.supported_reasoning_levels
|
|
.map((level) => level.effort)
|
|
.filter((effort): effort is string => Boolean(effort))
|
|
.map((effort) => ({
|
|
id: effort,
|
|
label: effort === 'xhigh' ? 'Extra High' : labelFromModelId(effort)
|
|
})),
|
|
defaultThinkingLevel: model.default_reasoning_level ?? 'low'
|
|
}
|
|
: {})
|
|
}))
|
|
)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function parseLineModels(stdout: string): CommitMessageModel[] {
|
|
const models: CommitMessageModel[] = []
|
|
for (const rawLine of iterateModelOutputLines(stdout)) {
|
|
const id = rawLine.trim()
|
|
if (id.length === 0 || id.includes(' ')) {
|
|
continue
|
|
}
|
|
models.push({
|
|
id,
|
|
label: labelFromModelId(id),
|
|
...withOpenAiThinking(id)
|
|
})
|
|
}
|
|
return uniqueModels(models)
|
|
}
|
|
|
|
export function parsePiModels(stdout: string): CommitMessageModel[] {
|
|
const models: CommitMessageModel[] = []
|
|
for (const rawLine of iterateModelOutputLines(stdout)) {
|
|
const parts = getPiModelTableFields(rawLine, 6)
|
|
if (parts.length < 6 || parts[0] === 'provider') {
|
|
continue
|
|
}
|
|
|
|
const [provider, model, , , thinking] = parts
|
|
const id = `${provider}/${model}`
|
|
models.push({
|
|
id,
|
|
label: `${labelFromModelId(provider)} ${labelFromModelId(model)}`,
|
|
...(thinking === 'yes'
|
|
? {
|
|
thinkingLevels: [
|
|
{ id: 'off', label: 'Off' },
|
|
{ id: 'low', label: 'Low' },
|
|
{ id: 'medium', label: 'Medium' },
|
|
{ id: 'high', label: 'High' },
|
|
{ id: 'xhigh', label: 'Extra High' }
|
|
],
|
|
defaultThinkingLevel: 'low'
|
|
}
|
|
: {})
|
|
})
|
|
}
|
|
return uniqueModels(models)
|
|
}
|
|
|
|
// Why: model discovery output can include paste-sized noisy lines; only the first fields matter.
|
|
function getPiModelTableFields(line: string, maxFields: number): string[] {
|
|
const fields: string[] = []
|
|
let tokenStart = -1
|
|
|
|
for (let index = 0; index <= line.length; index += 1) {
|
|
const isEnd = index === line.length
|
|
if (!isEnd && !isPiModelTableWhitespace(line.charCodeAt(index))) {
|
|
if (tokenStart === -1) {
|
|
tokenStart = index
|
|
}
|
|
continue
|
|
}
|
|
if (tokenStart !== -1) {
|
|
fields.push(line.slice(tokenStart, index))
|
|
tokenStart = -1
|
|
if (fields.length >= maxFields) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
function isPiModelTableWhitespace(code: number): boolean {
|
|
return (
|
|
code === 32 ||
|
|
(code >= 9 && code <= 13) ||
|
|
code === 160 ||
|
|
code === 5760 ||
|
|
(code >= 8192 && code <= 8202) ||
|
|
code === 8232 ||
|
|
code === 8233 ||
|
|
code === 8239 ||
|
|
code === 8287 ||
|
|
code === 12288 ||
|
|
code === 65279
|
|
)
|
|
}
|
|
|
|
export function parseCursorModels(stdout: string): CommitMessageModel[] {
|
|
const models: CommitMessageModel[] = []
|
|
for (const rawLine of iterateModelOutputLines(stdout)) {
|
|
const match = /^([^\s]+)\s+-\s+(.+)$/.exec(rawLine.trim())
|
|
if (!match) {
|
|
continue
|
|
}
|
|
models.push({
|
|
id: match[1],
|
|
label: match[2].replace(/\s+\((?:default|current)\)$/i, ''),
|
|
...withOpenAiThinking(match[1])
|
|
})
|
|
}
|
|
return uniqueModels(models)
|
|
}
|
|
|
|
export function parseAntigravityModels(stdout: string): CommitMessageModel[] {
|
|
const models: CommitMessageModel[] = []
|
|
for (const rawLine of iterateModelOutputLines(stdout)) {
|
|
const line = rawLine.trim()
|
|
const separator = line.indexOf('\t')
|
|
const id = (separator === -1 ? line : line.slice(0, separator)).trim()
|
|
const label = separator === -1 ? id : line.slice(separator + 1).trim()
|
|
// Older agy versions list display names; current versions emit id<TAB>label.
|
|
if (
|
|
!id ||
|
|
!label ||
|
|
(separator === -1 && !/^.+ \((?:low|medium|high|thinking)\)$/i.test(id)) ||
|
|
(separator !== -1 && (/\s/.test(id) || /^(?:id|model)$/i.test(id)))
|
|
) {
|
|
continue
|
|
}
|
|
models.push({
|
|
id,
|
|
label
|
|
})
|
|
}
|
|
return uniqueModels(models)
|
|
}
|