feat: add registry-driven Kimi provider

This commit is contained in:
tommy0103
2026-07-20 22:43:23 +08:00
parent 21c3a1b9fc
commit 3ee44de4e5
39 changed files with 2240 additions and 732 deletions
+7 -5
View File
@@ -17,6 +17,7 @@ import {
import { formatProjectLabel } from './utils.js';
import { buildSidebarProjects } from './sidebar-projects.mjs';
import { resolveGlobalShortcut } from './keyboard-shortcuts.mjs';
import { sourceLabel } from './source-catalog.mjs';
const router = useRouter();
const route = useRoute();
@@ -207,8 +208,9 @@ const showSourcePopover = ref(false);
async function loadSourceDots() {
if (!window.obelisk?.getSettings) return;
const s = await window.obelisk.getSettings();
sourceDots.value = (s.sources || []).map(src => ({ id: src.id, status: src.status }));
sourceDots.value = (s.sources || []).map(src => ({ id: src.id, status: src.status, color: src.color }));
sourceDetails.value = s.sources || [];
state.sources = s.sources || [];
}
loadSourceDots();
@@ -223,7 +225,7 @@ const showSourceFilter = ref(false);
const sourceFilterActive = computed(() => state.sourceFilter !== 'all' && state.sourceFilter !== undefined);
const sourceFilterLabel = computed(() => {
if (!state.sourceFilter || state.sourceFilter === 'all') return 'All sources';
return state.sourceFilter === 'claude' ? 'Claude Code' : 'Codex';
return sourceLabel(state.sourceFilter, state.sources);
});
function toggleSourceFilter() { showSourceFilter.value = !showSourceFilter.value; }
function setSourceFilter(id) {
@@ -271,13 +273,13 @@ provide('recapGenerateOpen', recapGenerateOpen);
</svg>
<span class="name">Obelisk</span>
<button class="source-health" title="Connected sources" @click="showSourcePopover = !showSourcePopover">
<span v-for="src in sourceDots" :key="src.id" class="h-dot" :class="src.id + '-' + src.status"></span>
<span v-for="src in sourceDots" :key="src.id" class="h-dot" :class="src.status" :style="{ '--source-color': src.color }"></span>
</button>
<div class="sources-popover" :class="{ show: showSourcePopover }">
<div class="sp-head">Connected sources</div>
<div class="sp-list">
<button v-for="src in sourceDetails" :key="src.id" class="sp-row" @click="router.push('/settings')">
<span class="sp-dot" :class="src.id"></span>
<span class="sp-dot" :style="{ '--source-color': src.color }"></span>
<div class="sp-body">
<div class="sp-name">{{ src.name }} <span class="sp-count" v-if="src.sessionCount">{{ src.sessionCount }} sessions</span></div>
<div class="sp-meta" :class="src.status">{{ src.statusText }}</div>
@@ -535,7 +537,7 @@ provide('recapGenerateOpen', recapGenerateOpen);
<div class="fd-check">
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6l2.5 2.5 4.5-5"/></svg>
</div>
<span class="fd-name">{{ src.id === 'claude' ? 'Claude Code' : 'Codex' }}</span>
<span class="fd-name">{{ sourceLabel(src.id, sourceDetails) }}</span>
</div>
<div class="fd-divider"></div>
<div class="fd-row" :class="{ checked: state.sourceFilter === 'all' }" @click.stop="setSourceFilter('all')">
+6 -6
View File
@@ -1,13 +1,12 @@
import { sourceLabel } from './source-catalog.mjs';
export function activitySourceKey(session) {
const source = typeof session?.source === 'string' ? session.source.trim().toLowerCase() : '';
return source || 'claude';
}
export function activitySourceLabel(session) {
const source = activitySourceKey(session);
if (source === 'codex') return 'Codex';
if (source === 'claude') return 'Claude Code';
return source.charAt(0).toUpperCase() + source.slice(1);
export function activitySourceLabel(session, sourceCatalog = []) {
return sourceLabel(activitySourceKey(session), sourceCatalog);
}
export function activityGroupSessions(split) {
@@ -22,9 +21,10 @@ export function activitySessionMetaParts(session, {
mixedSources = false,
projectLabel = '',
includeProject = true,
sourceCatalog = [],
} = {}) {
const parts = [];
if (mixedSources) parts.push({ kind: 'source', text: activitySourceLabel(session) });
if (mixedSources) parts.push({ kind: 'source', text: activitySourceLabel(session, sourceCatalog) });
if (includeProject && projectLabel) parts.push({ kind: 'project', text: projectLabel });
parts.push({
kind: 'count',
@@ -2,6 +2,7 @@
import { computed } from 'vue';
import { formatProjectLabel } from '../utils.js';
import { activitySessionMetaParts } from '../activity-ledger.mjs';
import { state } from '../store.js';
const props = defineProps({
session: { type: Object, required: true },
@@ -17,6 +18,7 @@ const metaParts = computed(() => activitySessionMetaParts(props.session, {
mixedSources: props.mixedSources,
projectLabel: formatProjectLabel(props.session.project) || '',
includeProject: props.includeProject,
sourceCatalog: state.sources,
}));
</script>
+28
View File
@@ -0,0 +1,28 @@
const FALLBACK_COLOR = '#8b8b93';
function sourceId(value) {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
return normalized || 'claude';
}
function descriptorFor(source, catalog = []) {
const id = sourceId(source);
return catalog.find(candidate => candidate?.id === id) || null;
}
function titleCaseId(id) {
return id
.split(/[-_]+/)
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
export function sourceLabel(source, catalog = []) {
const id = sourceId(source);
return descriptorFor(id, catalog)?.name || titleCaseId(id);
}
export function sourceColor(source, catalog = []) {
return descriptorFor(source, catalog)?.color || FALLBACK_COLOR;
}
+1
View File
@@ -8,6 +8,7 @@ export const state = reactive({
sessions: [],
sessionTitleOverrides: shallowReactive(new Map()),
projects: [],
sources: [],
stats: {},
view: 'active', // 'active' | 'archived'
query: '',
+3 -2
View File
@@ -23,6 +23,7 @@ import {
fmtRelative,
formatProjectLabel
} from '../utils.js';
import { sourceColor, sourceLabel } from '../source-catalog.mjs';
defineOptions({ name: 'SessionDetail' });
const props = defineProps({ id: String });
@@ -490,8 +491,8 @@ function navigateToSubagent(agentId) {
<span class="sep">&middot;</span>
<span class="project-path">{{ session.project_path || '' }}</span>
<span class="via">
<span class="via-dot" :class="session.source || 'claude'"></span>
via {{ (session.source || 'claude') === 'codex' ? 'Codex' : 'Claude Code' }}
<span class="via-dot" :style="{ '--source-color': sourceColor(session.source, state.sources) }"></span>
via {{ sourceLabel(session.source, state.sources) }}
</span>
</div>
<div class="session-title">{{ session.title || '(untitled)' }}</div>
+3 -6
View File
@@ -29,8 +29,7 @@ async function browseSourcePath(source) {
if (!window.obelisk?.browseFolder) return;
const result = await window.obelisk.browseFolder();
if (result) {
const key = source.id === 'claude' ? 'claudeDir' : 'codexDir';
await saveSetting(key, result);
await saveSetting(source.settingKey || `providerRoots.${source.id}`, result);
await loadSettings();
}
}
@@ -107,8 +106,8 @@ function fmtRelative(iso) {
:class="{ error: src.status === 'error', warn: src.status === 'warn' }"
>
<div class="source-card-head">
<div class="source-card-mark" :class="src.id">
<span class="mark-dot"></span>
<div class="source-card-mark">
<span class="mark-dot" :style="{ background: src.color, boxShadow: `0 0 6px ${src.color}80` }"></span>
</div>
<div class="source-card-info">
<div class="source-card-name">
@@ -261,8 +260,6 @@ function fmtRelative(iso) {
display: grid; place-items: center; flex-shrink: 0;
}
.source-card-mark .mark-dot { width: 8px; height: 8px; border-radius: 50%; }
.source-card-mark.claude .mark-dot { background: #d97757; box-shadow: 0 0 6px rgba(217,119,87,0.5); }
.source-card-mark.codex .mark-dot { background: #10a37f; box-shadow: 0 0 6px rgba(16,163,127,0.5); }
.source-card-info { flex: 1; min-width: 0; }
.source-card-name {
font-size: 14px; color: var(--fg); font-weight: 600; letter-spacing: -0.005em;
+1 -2
View File
@@ -434,8 +434,7 @@
margin-left: 6px;
}
.session-eyebrow .via .via-dot { width: 4px; height: 4px; border-radius: 50%; }
.session-eyebrow .via .via-dot.claude { background: #d97757; box-shadow: 0 0 4px rgba(217,119,87,0.5); }
.session-eyebrow .via .via-dot.codex { background: #10a37f; box-shadow: 0 0 4px rgba(16,163,127,0.5); }
.session-eyebrow .via .via-dot { background: var(--source-color); box-shadow: 0 0 4px var(--source-color); }
.session-title {
font-size: 22px; font-weight: 600; color: var(--fg);
line-height: 1.3; margin-bottom: 14px; letter-spacing: -0.01em;
+4 -8
View File
@@ -29,12 +29,9 @@
.source-health .h-dot {
width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0;
}
.source-health .h-dot.claude-ok { background: #d97757; box-shadow: 0 0 4px rgba(217,119,87,0.6); }
.source-health .h-dot.claude-warn { background: rgba(217,119,87,0.4); }
.source-health .h-dot.claude-error { background: rgba(217,119,87,0.25); }
.source-health .h-dot.codex-ok { background: #10a37f; box-shadow: 0 0 4px rgba(16,163,127,0.6); }
.source-health .h-dot.codex-warn { background: rgba(16,163,127,0.4); }
.source-health .h-dot.codex-error { background: rgba(16,163,127,0.25); }
.source-health .h-dot.ok { background: var(--source-color); box-shadow: 0 0 4px var(--source-color); }
.source-health .h-dot.warn { background: color-mix(in srgb, var(--source-color) 40%, transparent); }
.source-health .h-dot.error { background: color-mix(in srgb, var(--source-color) 25%, transparent); }
.source-health .h-dot.off { background: var(--muted-3); }
/* Sources popover */
@@ -60,8 +57,7 @@
}
.sp-row:hover { background: rgba(255,255,255,0.03); }
.sp-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.sp-dot.claude { background: #d97757; box-shadow: 0 0 6px rgba(217,119,87,0.5); }
.sp-dot.codex { background: #10a37f; box-shadow: 0 0 6px rgba(16,163,127,0.5); }
.sp-dot { background: var(--source-color); box-shadow: 0 0 6px var(--source-color); }
.sp-dot.off { background: var(--muted-3); }
.sp-body { flex: 1; min-width: 0; }
.sp-name { font-size: 12.5px; color: var(--fg); font-weight: 500; display: flex; align-items: baseline; gap: 6px; }