feat: add registry-driven Kimi provider
This commit is contained in:
@@ -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')">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export const state = reactive({
|
||||
sessions: [],
|
||||
sessionTitleOverrides: shallowReactive(new Map()),
|
||||
projects: [],
|
||||
sources: [],
|
||||
stats: {},
|
||||
view: 'active', // 'active' | 'archived'
|
||||
query: '',
|
||||
|
||||
@@ -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">·</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user