katashiro 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * KnowledgeViewProvider - Knowledge Graph panel tree view
3
+ *
4
+ * @module katashiro
5
+ * @task TSK-072
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+
10
+ /**
11
+ * Knowledge item for tree view
12
+ */
13
+ export class KnowledgeItem extends vscode.TreeItem {
14
+ constructor(
15
+ public readonly label: string,
16
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
17
+ public readonly itemType: 'category' | 'node' | 'edge',
18
+ public readonly description?: string
19
+ ) {
20
+ super(label, collapsibleState);
21
+ this.description = description;
22
+ this.contextValue = itemType;
23
+
24
+ // Set icon based on type
25
+ switch (itemType) {
26
+ case 'category':
27
+ this.iconPath = new vscode.ThemeIcon('folder');
28
+ break;
29
+ case 'node':
30
+ this.iconPath = new vscode.ThemeIcon('circle-filled');
31
+ break;
32
+ case 'edge':
33
+ this.iconPath = new vscode.ThemeIcon('arrow-right');
34
+ break;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * KnowledgeViewProvider
41
+ *
42
+ * Provides tree data for the Knowledge Graph panel
43
+ */
44
+ export class KnowledgeViewProvider
45
+ implements vscode.TreeDataProvider<KnowledgeItem>
46
+ {
47
+ private _onDidChangeTreeData = new vscode.EventEmitter<
48
+ KnowledgeItem | undefined | null | void
49
+ >();
50
+ readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
51
+
52
+ private nodes: Array<{ id: string; label: string; type: string }> = [];
53
+ private edges: Array<{ source: string; target: string; relation: string }> =
54
+ [];
55
+
56
+ /**
57
+ * Refresh the tree view
58
+ */
59
+ refresh(): void {
60
+ this._onDidChangeTreeData.fire();
61
+ }
62
+
63
+ /**
64
+ * Add a node
65
+ */
66
+ addNode(id: string, label: string, type: string): void {
67
+ this.nodes.push({ id, label, type });
68
+ this.refresh();
69
+ }
70
+
71
+ /**
72
+ * Add an edge
73
+ */
74
+ addEdge(source: string, target: string, relation: string): void {
75
+ this.edges.push({ source, target, relation });
76
+ this.refresh();
77
+ }
78
+
79
+ /**
80
+ * Clear all data
81
+ */
82
+ clear(): void {
83
+ this.nodes = [];
84
+ this.edges = [];
85
+ this.refresh();
86
+ }
87
+
88
+ /**
89
+ * Get statistics
90
+ */
91
+ getStats(): { nodes: number; edges: number } {
92
+ return {
93
+ nodes: this.nodes.length,
94
+ edges: this.edges.length,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Get tree item
100
+ */
101
+ getTreeItem(element: KnowledgeItem): vscode.TreeItem {
102
+ return element;
103
+ }
104
+
105
+ /**
106
+ * Get children
107
+ */
108
+ getChildren(element?: KnowledgeItem): Thenable<KnowledgeItem[]> {
109
+ if (!element) {
110
+ // Root level - categories
111
+ const stats = this.getStats();
112
+ return Promise.resolve([
113
+ new KnowledgeItem(
114
+ 'Nodes',
115
+ vscode.TreeItemCollapsibleState.Collapsed,
116
+ 'category',
117
+ `${stats.nodes} items`
118
+ ),
119
+ new KnowledgeItem(
120
+ 'Edges',
121
+ vscode.TreeItemCollapsibleState.Collapsed,
122
+ 'category',
123
+ `${stats.edges} items`
124
+ ),
125
+ new KnowledgeItem(
126
+ 'Actions',
127
+ vscode.TreeItemCollapsibleState.Collapsed,
128
+ 'category'
129
+ ),
130
+ ]);
131
+ }
132
+
133
+ // Children based on category
134
+ switch (element.label) {
135
+ case 'Nodes':
136
+ if (this.nodes.length === 0) {
137
+ return Promise.resolve([
138
+ new KnowledgeItem(
139
+ 'No nodes yet',
140
+ vscode.TreeItemCollapsibleState.None,
141
+ 'node',
142
+ 'Start researching to add knowledge'
143
+ ),
144
+ ]);
145
+ }
146
+ return Promise.resolve(
147
+ this.nodes.map(
148
+ (node) =>
149
+ new KnowledgeItem(
150
+ node.label,
151
+ vscode.TreeItemCollapsibleState.None,
152
+ 'node',
153
+ node.type
154
+ )
155
+ )
156
+ );
157
+
158
+ case 'Edges':
159
+ if (this.edges.length === 0) {
160
+ return Promise.resolve([
161
+ new KnowledgeItem(
162
+ 'No edges yet',
163
+ vscode.TreeItemCollapsibleState.None,
164
+ 'edge',
165
+ 'Connections between nodes'
166
+ ),
167
+ ]);
168
+ }
169
+ return Promise.resolve(
170
+ this.edges.map(
171
+ (edge) =>
172
+ new KnowledgeItem(
173
+ `${edge.source} → ${edge.target}`,
174
+ vscode.TreeItemCollapsibleState.None,
175
+ 'edge',
176
+ edge.relation
177
+ )
178
+ )
179
+ );
180
+
181
+ case 'Actions':
182
+ return Promise.resolve([
183
+ new KnowledgeItem(
184
+ 'View Graph',
185
+ vscode.TreeItemCollapsibleState.None,
186
+ 'category'
187
+ ),
188
+ new KnowledgeItem(
189
+ 'Export',
190
+ vscode.TreeItemCollapsibleState.None,
191
+ 'category'
192
+ ),
193
+ new KnowledgeItem(
194
+ 'Clear All',
195
+ vscode.TreeItemCollapsibleState.None,
196
+ 'category'
197
+ ),
198
+ ]);
199
+
200
+ default:
201
+ return Promise.resolve([]);
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ResearchViewProvider - Research panel tree view
3
+ *
4
+ * @module katashiro
5
+ * @task TSK-072
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+
10
+ /**
11
+ * Research item for tree view
12
+ */
13
+ export class ResearchItem extends vscode.TreeItem {
14
+ constructor(
15
+ public readonly label: string,
16
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
17
+ public readonly description?: string,
18
+ public readonly command?: vscode.Command
19
+ ) {
20
+ super(label, collapsibleState);
21
+ this.description = description;
22
+ this.command = command;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * ResearchViewProvider
28
+ *
29
+ * Provides tree data for the Research panel
30
+ */
31
+ export class ResearchViewProvider
32
+ implements vscode.TreeDataProvider<ResearchItem>
33
+ {
34
+ private _onDidChangeTreeData = new vscode.EventEmitter<
35
+ ResearchItem | undefined | null | void
36
+ >();
37
+ readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
38
+
39
+ private recentSearches: Array<{ query: string; timestamp: Date }> = [];
40
+
41
+ /**
42
+ * Refresh the tree view
43
+ */
44
+ refresh(): void {
45
+ this._onDidChangeTreeData.fire();
46
+ }
47
+
48
+ /**
49
+ * Add a recent search
50
+ */
51
+ addSearch(query: string): void {
52
+ this.recentSearches.unshift({ query, timestamp: new Date() });
53
+ if (this.recentSearches.length > 10) {
54
+ this.recentSearches.pop();
55
+ }
56
+ this.refresh();
57
+ }
58
+
59
+ /**
60
+ * Get tree item
61
+ */
62
+ getTreeItem(element: ResearchItem): vscode.TreeItem {
63
+ return element;
64
+ }
65
+
66
+ /**
67
+ * Get children
68
+ */
69
+ getChildren(element?: ResearchItem): Thenable<ResearchItem[]> {
70
+ if (!element) {
71
+ // Root level items
72
+ return Promise.resolve([
73
+ new ResearchItem(
74
+ 'New Search',
75
+ vscode.TreeItemCollapsibleState.None,
76
+ '',
77
+ {
78
+ command: 'katashiro.webSearch',
79
+ title: 'New Search',
80
+ }
81
+ ),
82
+ new ResearchItem(
83
+ 'Research Topic',
84
+ vscode.TreeItemCollapsibleState.None,
85
+ '',
86
+ {
87
+ command: 'katashiro.researchTopic',
88
+ title: 'Research Topic',
89
+ }
90
+ ),
91
+ new ResearchItem(
92
+ 'Recent Searches',
93
+ vscode.TreeItemCollapsibleState.Expanded
94
+ ),
95
+ ]);
96
+ }
97
+
98
+ // Recent searches children
99
+ if (element.label === 'Recent Searches') {
100
+ if (this.recentSearches.length === 0) {
101
+ return Promise.resolve([
102
+ new ResearchItem(
103
+ 'No recent searches',
104
+ vscode.TreeItemCollapsibleState.None,
105
+ 'Start a new search'
106
+ ),
107
+ ]);
108
+ }
109
+
110
+ return Promise.resolve(
111
+ this.recentSearches.map(
112
+ (search) =>
113
+ new ResearchItem(
114
+ search.query,
115
+ vscode.TreeItemCollapsibleState.None,
116
+ this.formatTime(search.timestamp)
117
+ )
118
+ )
119
+ );
120
+ }
121
+
122
+ return Promise.resolve([]);
123
+ }
124
+
125
+ /**
126
+ * Format timestamp
127
+ */
128
+ private formatTime(date: Date): string {
129
+ const now = new Date();
130
+ const diffMs = now.getTime() - date.getTime();
131
+ const diffMins = Math.floor(diffMs / 60000);
132
+
133
+ if (diffMins < 1) return 'just now';
134
+ if (diffMins < 60) return `${diffMins}m ago`;
135
+
136
+ const diffHours = Math.floor(diffMins / 60);
137
+ if (diffHours < 24) return `${diffHours}h ago`;
138
+
139
+ return date.toLocaleDateString();
140
+ }
141
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * VS Code Mock - テスト用モック
3
+ *
4
+ * VS Code APIのモックを提供
5
+ */
6
+
7
+ export const window = {
8
+ showInformationMessage: vi.fn(),
9
+ showWarningMessage: vi.fn(),
10
+ showErrorMessage: vi.fn(),
11
+ showInputBox: vi.fn(),
12
+ showQuickPick: vi.fn(),
13
+ withProgress: vi.fn(async (_options, task) => {
14
+ return task({ report: vi.fn() }, { isCancellationRequested: false });
15
+ }),
16
+ createOutputChannel: vi.fn(() => ({
17
+ appendLine: vi.fn(),
18
+ clear: vi.fn(),
19
+ show: vi.fn(),
20
+ dispose: vi.fn(),
21
+ })),
22
+ createStatusBarItem: vi.fn(() => ({
23
+ text: '',
24
+ tooltip: '',
25
+ command: undefined,
26
+ backgroundColor: undefined,
27
+ show: vi.fn(),
28
+ hide: vi.fn(),
29
+ dispose: vi.fn(),
30
+ })),
31
+ activeTextEditor: undefined,
32
+ registerTreeDataProvider: vi.fn(() => ({ dispose: vi.fn() })),
33
+ };
34
+
35
+ export const workspace = {
36
+ getConfiguration: vi.fn(() => ({
37
+ get: vi.fn((key: string, defaultValue: unknown) => defaultValue),
38
+ })),
39
+ openTextDocument: vi.fn(async (options) => ({
40
+ getText: vi.fn(() => options.content || ''),
41
+ uri: { fsPath: '/test/file.md' },
42
+ })),
43
+ };
44
+
45
+ export const commands = {
46
+ registerCommand: vi.fn(() => ({ dispose: vi.fn() })),
47
+ executeCommand: vi.fn(),
48
+ };
49
+
50
+ export enum TreeItemCollapsibleState {
51
+ None = 0,
52
+ Collapsed = 1,
53
+ Expanded = 2,
54
+ }
55
+
56
+ export class TreeItem {
57
+ label: string;
58
+ collapsibleState: TreeItemCollapsibleState;
59
+ description?: string;
60
+ tooltip?: string;
61
+ command?: Command;
62
+ iconPath?: ThemeIcon;
63
+ contextValue?: string;
64
+
65
+ constructor(
66
+ label: string,
67
+ collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None
68
+ ) {
69
+ this.label = label;
70
+ this.collapsibleState = collapsibleState;
71
+ }
72
+ }
73
+
74
+ export interface Command {
75
+ command: string;
76
+ title: string;
77
+ arguments?: unknown[];
78
+ }
79
+
80
+ export class ThemeIcon {
81
+ constructor(public readonly id: string) {}
82
+ }
83
+
84
+ export class ThemeColor {
85
+ constructor(public readonly id: string) {}
86
+ }
87
+
88
+ export enum StatusBarAlignment {
89
+ Left = 1,
90
+ Right = 2,
91
+ }
92
+
93
+ export class EventEmitter<T> {
94
+ private listeners: Array<(e: T) => void> = [];
95
+
96
+ event = (listener: (e: T) => void) => {
97
+ this.listeners.push(listener);
98
+ return { dispose: () => {} };
99
+ };
100
+
101
+ fire(data: T): void {
102
+ this.listeners.forEach((l) => l(data));
103
+ }
104
+ }
105
+
106
+ export enum ProgressLocation {
107
+ Notification = 15,
108
+ Window = 10,
109
+ SourceControl = 1,
110
+ }
111
+
112
+ export enum ViewColumn {
113
+ Active = -1,
114
+ Beside = -2,
115
+ One = 1,
116
+ Two = 2,
117
+ Three = 3,
118
+ }
119
+
120
+ // Re-export vi for test files
121
+ import { vi } from 'vitest';
@@ -0,0 +1,150 @@
1
+ /**
2
+ * HistoryViewProvider テスト
3
+ *
4
+ * @task TSK-072
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+
9
+ // Mock vscode module
10
+ vi.mock('vscode', () => import('../mocks/vscode.js'));
11
+
12
+ import {
13
+ HistoryViewProvider,
14
+ HistoryItem,
15
+ } from '../../src/views/history-view-provider.js';
16
+
17
+ describe('HistoryViewProvider', () => {
18
+ let provider: HistoryViewProvider;
19
+
20
+ beforeEach(() => {
21
+ provider = new HistoryViewProvider();
22
+ });
23
+
24
+ describe('initialization', () => {
25
+ it('should create provider', () => {
26
+ expect(provider).toBeDefined();
27
+ });
28
+
29
+ it('should have empty history initially', () => {
30
+ const all = provider.getAll();
31
+ expect(all.length).toBe(0);
32
+ });
33
+ });
34
+
35
+ describe('addEntry', () => {
36
+ it('should add entry', () => {
37
+ const entry = provider.addEntry('search', 'Test Search', 'Details');
38
+
39
+ expect(entry.id).toBeDefined();
40
+ expect(entry.type).toBe('search');
41
+ expect(entry.title).toBe('Test Search');
42
+ expect(entry.details).toBe('Details');
43
+ expect(entry.timestamp).toBeInstanceOf(Date);
44
+ });
45
+
46
+ it('should add multiple entries in order', () => {
47
+ provider.addEntry('search', 'First');
48
+ provider.addEntry('analysis', 'Second');
49
+ provider.addEntry('summary', 'Third');
50
+
51
+ const all = provider.getAll();
52
+ expect(all.length).toBe(3);
53
+ expect(all[0].title).toBe('Third'); // Most recent first
54
+ expect(all[2].title).toBe('First');
55
+ });
56
+
57
+ it('should limit to 50 entries', () => {
58
+ for (let i = 0; i < 60; i++) {
59
+ provider.addEntry('search', `Entry ${i}`);
60
+ }
61
+
62
+ const all = provider.getAll();
63
+ expect(all.length).toBe(50);
64
+ });
65
+ });
66
+
67
+ describe('getEntry', () => {
68
+ it('should get entry by ID', () => {
69
+ const added = provider.addEntry('search', 'Test');
70
+ const found = provider.getEntry(added.id);
71
+
72
+ expect(found).toBeDefined();
73
+ expect(found?.id).toBe(added.id);
74
+ });
75
+
76
+ it('should return undefined for unknown ID', () => {
77
+ const found = provider.getEntry('unknown-id');
78
+ expect(found).toBeUndefined();
79
+ });
80
+ });
81
+
82
+ describe('clear', () => {
83
+ it('should clear all entries', () => {
84
+ provider.addEntry('search', 'Test 1');
85
+ provider.addEntry('analysis', 'Test 2');
86
+
87
+ provider.clear();
88
+
89
+ const all = provider.getAll();
90
+ expect(all.length).toBe(0);
91
+ });
92
+ });
93
+
94
+ describe('getChildren', () => {
95
+ it('should return empty array when no history', async () => {
96
+ const children = await provider.getChildren();
97
+ expect(children.length).toBe(0);
98
+ });
99
+
100
+ it('should return history items', async () => {
101
+ provider.addEntry('search', 'Search 1');
102
+ provider.addEntry('analysis', 'Analysis 1');
103
+
104
+ const children = await provider.getChildren();
105
+ expect(children.length).toBe(2);
106
+ });
107
+
108
+ it('should return no children for items', async () => {
109
+ provider.addEntry('search', 'Test');
110
+ const children = await provider.getChildren();
111
+ const itemChildren = await provider.getChildren(children[0]);
112
+
113
+ expect(itemChildren.length).toBe(0);
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('HistoryItem', () => {
119
+ it('should create item from entry', () => {
120
+ const entry = {
121
+ id: 'test-id',
122
+ type: 'search' as const,
123
+ title: 'Test Search',
124
+ timestamp: new Date(),
125
+ details: 'Test details',
126
+ };
127
+
128
+ const item = new HistoryItem(entry, 0);
129
+
130
+ expect(item.entry).toBe(entry);
131
+ expect(item.label).toBe('Test Search');
132
+ expect(item.contextValue).toBe('search');
133
+ });
134
+
135
+ it('should set icon based on type', () => {
136
+ const types = ['search', 'analysis', 'summary', 'research', 'report'] as const;
137
+
138
+ for (const type of types) {
139
+ const entry = {
140
+ id: `${type}-id`,
141
+ type,
142
+ title: `Test ${type}`,
143
+ timestamp: new Date(),
144
+ };
145
+
146
+ const item = new HistoryItem(entry, 0);
147
+ expect(item.iconPath).toBeDefined();
148
+ }
149
+ });
150
+ });